feat: add DISC profiles and a sales copilot as an org module
Help reps capture a customer's likely behavior style and get negotiation guidance, with the feature gated from super admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,839 @@
|
||||
/** تحلیل رفتاری DISC و دستیار فروش — راهنمای مذاکره، نه تشخیص بالینی. */
|
||||
|
||||
export const DISC_TYPES = ["D", "I", "S", "C"] as const;
|
||||
export type DiscType = (typeof DISC_TYPES)[number];
|
||||
|
||||
export const DISC_TYPE_LABELS: Record<DiscType, string> = {
|
||||
D: "سلطهگری (نتیجهگرا)",
|
||||
I: "تأثیرگذاری (ارتباطگرا)",
|
||||
S: "ثبات (رابطهمحور)",
|
||||
C: "وظیفهشناسی (دقیق)",
|
||||
};
|
||||
|
||||
export const DISC_TYPE_SHORT: Record<DiscType, string> = {
|
||||
D: "نتیجهگرا",
|
||||
I: "ارتباطگرا",
|
||||
S: "باثبات",
|
||||
C: "دقیق",
|
||||
};
|
||||
|
||||
export const DISC_MIN_VALID_ANSWERS = 4;
|
||||
export const DISC_QUESTION_COUNT = 8;
|
||||
/** پروفایل قدیمیتر از این روز در دستیار فقط بهعنوان سرنخ ضعیف استفاده نمیشود. */
|
||||
export const DISC_FRESH_DAYS = 180;
|
||||
|
||||
export const DISC_INSUFFICIENT_LABEL = "اطلاعات ناکافی برای نتیجه قابل اتکا";
|
||||
|
||||
export type DiscConfidence = "insufficient" | "low" | "medium" | "high";
|
||||
|
||||
export const DISC_CONFIDENCE_LABELS: Record<DiscConfidence, string> = {
|
||||
insufficient: "ناکافی",
|
||||
low: "کم",
|
||||
medium: "متوسط",
|
||||
high: "نسبتاً روشن",
|
||||
};
|
||||
|
||||
export type DiscAnswers = Partial<Record<string, DiscType | null>>;
|
||||
|
||||
export interface DiscOption {
|
||||
key: "A" | "B" | "C" | "D";
|
||||
type: DiscType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface DiscQuestion {
|
||||
id: string;
|
||||
prompt: string;
|
||||
options: DiscOption[];
|
||||
}
|
||||
|
||||
export const DISC_QUESTIONS: DiscQuestion[] = [
|
||||
{
|
||||
id: "1",
|
||||
prompt: "مشتری در ابتدای مکالمه چگونه رفتار میکند؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "سریع سراغ اصل موضوع، نتیجه و قیمت میرود." },
|
||||
{ key: "B", type: "I", text: "گرم و صمیمی صحبت میکند و ارتباط برقرار میکند." },
|
||||
{ key: "C", type: "S", text: "با آرامش گوش میدهد و برای صحبتکردن فرصت میخواهد." },
|
||||
{ key: "D", type: "C", text: "سؤالهای دقیق درباره جزئیات، قابلیتها و شرایط میپرسد." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
prompt: "هنگام تصمیمگیری چه رفتاری دارد؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "سریع تصمیم میگیرد و دوست دارد کنترل انتخاب دست خودش باشد." },
|
||||
{ key: "B", type: "I", text: "با توجه به حس مثبتش نسبت به محصول و ارتباط با فروشنده تصمیم میگیرد." },
|
||||
{ key: "C", type: "S", text: "ترجیح میدهد فرصت کافی برای بررسی و مشورت داشته باشد." },
|
||||
{ key: "D", type: "C", text: "اطلاعات، اعداد و شواهد را بررسی میکند و سپس تصمیم میگیرد." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
prompt: "در برابر پیشنهاد فروش چه واکنشی نشان میدهد؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "میپرسد این محصول دقیقاً چه نتیجهای برای من دارد؟" },
|
||||
{ key: "B", type: "I", text: "درباره تجربه استفاده، مزایا و جذابیت محصول صحبت میکند." },
|
||||
{ key: "C", type: "S", text: "میخواهد مطمئن شود استفاده از محصول برایش راحت و بدون دردسر است." },
|
||||
{ key: "D", type: "C", text: "درباره جزئیات فنی، شرایط قرارداد، محدودیتها و مقایسه گزینهها میپرسد." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
prompt: "وقتی با مخالفت یا ابهام روبهرو میشود، چه میکند؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "مستقیم مخالفت میکند و به دنبال پاسخ روشن است." },
|
||||
{ key: "B", type: "I", text: "با صحبت و تعامل تلاش میکند موضوع را حل کند." },
|
||||
{ key: "C", type: "S", text: "از تنش دوری میکند و ترجیح میدهد با آرامش بررسی کند." },
|
||||
{ key: "D", type: "C", text: "برای رفع ابهام، دلیل و مدرک مشخص میخواهد." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
prompt: "کدام عامل بیشتر روی تصمیم خرید او اثر میگذارد؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "سرعت، نتیجه، مزیت رقابتی و بازدهی." },
|
||||
{ key: "B", type: "I", text: "تجربه مثبت، اعتماد ارتباطی و جذابیت پیشنهاد." },
|
||||
{ key: "C", type: "S", text: "اطمینان، پشتیبانی و ثبات در همکاری." },
|
||||
{ key: "D", type: "C", text: "دقت، کیفیت، شواهد و شفافیت شرایط." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
prompt: "در مکالمه بیشتر چگونه صحبت میکند؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "کوتاه، مستقیم و هدفمند." },
|
||||
{ key: "B", type: "I", text: "پرانرژی، صمیمی و همراه با مثال و داستان." },
|
||||
{ key: "C", type: "S", text: "آرام، شنونده و با ملاحظه." },
|
||||
{ key: "D", type: "C", text: "ساختارمند، دقیق و همراه با سؤالهای مشخص." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
prompt: "هنگام بررسی قیمت چه رفتاری دارد؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "سریع درباره ارزش پیشنهادی و نتیجه نهایی مذاکره میکند." },
|
||||
{ key: "B", type: "I", text: "با فروشنده وارد گفتوگو میشود و درباره مزایا و تجربه دیگران صحبت میکند." },
|
||||
{ key: "C", type: "S", text: "درباره شرایط پرداخت، پشتیبانی و اطمینان از خرید سؤال میکند." },
|
||||
{ key: "D", type: "C", text: "قیمت را با امکانات، هزینههای جانبی و گزینههای دیگر مقایسه میکند." },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
prompt: "چه چیزی باعث میشود اعتماد بیشتری پیدا کند؟",
|
||||
options: [
|
||||
{ key: "A", type: "D", text: "پاسخ قاطع، شفاف و نتیجهمحور." },
|
||||
{ key: "B", type: "I", text: "ارتباط خوب، تعامل صمیمانه و تجربه مثبت." },
|
||||
{ key: "C", type: "S", text: "پیگیری منظم، خوشقولی و پشتیبانی قابل اتکا." },
|
||||
{ key: "D", type: "C", text: "مستندات، اطلاعات دقیق و پاسخهای قابل بررسی." },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export interface DiscGuide {
|
||||
strengths: string[];
|
||||
negotiation: string[];
|
||||
presentation: string[];
|
||||
followUp: string[];
|
||||
}
|
||||
|
||||
export const DISC_GUIDES: Record<DiscType, DiscGuide> = {
|
||||
D: {
|
||||
strengths: ["تمرکز روی نتیجه", "تصمیم سریع", "گفتوگوی مستقیم"],
|
||||
negotiation: [
|
||||
"مکالمه کوتاه و هدفمند باشد.",
|
||||
"ابتدا نتیجه و ارزش اقتصادی پیشنهاد بیان شود.",
|
||||
"گزینهها و مراحل تصمیمگیری شفاف باشند.",
|
||||
"از توضیحات طولانی و حاشیهای پرهیز شود.",
|
||||
],
|
||||
presentation: ["نتیجه را قبل از جزئیات بگویید.", "یک گزینه اصلی و یک جایگزین کافی است."],
|
||||
followUp: ["پیگیری کوتاه و با زمان مشخص.", "روی تصمیم بعدی تمرکز کنید، نه رابطهٔ طولانی."],
|
||||
},
|
||||
I: {
|
||||
strengths: ["تعامل گرم", "علاقه به تجربه و داستان", "تأثیرپذیری از حس مثبت"],
|
||||
negotiation: [
|
||||
"ارتباط صمیمانه و تعاملی ایجاد شود.",
|
||||
"مزایا با مثال و تجربه واقعی توضیح داده شوند.",
|
||||
"فرصت سؤال و گفتوگو داده شود.",
|
||||
"پیگیری محترمانه و رابطهمحور باشد.",
|
||||
],
|
||||
presentation: ["از مثال مشتریان مشابه استفاده کنید، بدون اغراق.", "فضای گفتوگو باز بماند."],
|
||||
followUp: ["لحن گرم و شخصی باشد.", "به ادامهٔ گفتوگو دعوت کنید، نه فشار برای خرید."],
|
||||
},
|
||||
S: {
|
||||
strengths: ["صبر", "توجه به راحتی اجرا", "اهمیت ثبات و پشتیبانی"],
|
||||
negotiation: [
|
||||
"درباره پشتیبانی، آموزش و روند اجرا توضیح داده شود.",
|
||||
"به مشتری فرصت کافی برای تصمیمگیری داده شود.",
|
||||
"مراحل همکاری و تعهدات روشن باشند.",
|
||||
"پیگیری منظم و بدون فشار انجام شود.",
|
||||
],
|
||||
presentation: ["مسیر اجرا را قدمبهقدم بگویید.", "ریسک تغییر را کم نشان دهید، فقط اگر واقعاً کم است."],
|
||||
followUp: ["زمان پیگیری را با خود مشتری هماهنگ کنید.", "یادآوری آرام و منظم باشد."],
|
||||
},
|
||||
C: {
|
||||
strengths: ["دقت", "مقایسه", "نیاز به مدرک"],
|
||||
negotiation: [
|
||||
"اطلاعات دقیق و قابل بررسی ارائه شود.",
|
||||
"امکانات، محدودیتها و هزینهها شفاف باشند.",
|
||||
"مقایسه مستند و پاسخ فنی آماده باشد.",
|
||||
"برای بررسی پیشنهاد فرصت داده شود.",
|
||||
],
|
||||
presentation: ["ادعا را بدون عدد یا سند ثبتشده مطرح نکنید.", "محدودیتها را هم بگویید."],
|
||||
followUp: ["مستندات را بفرستید و زمان بررسی را بپرسید.", "پیگیری برای پاسخ به سؤال باشد، نه تعجیل."],
|
||||
},
|
||||
};
|
||||
|
||||
export interface DiscScoreResult {
|
||||
scores: Record<DiscType, number>;
|
||||
percents: Record<DiscType, number>;
|
||||
validCount: number;
|
||||
skippedCount: number;
|
||||
reliable: boolean;
|
||||
primaryTypes: DiscType[];
|
||||
secondaryTypes: DiscType[];
|
||||
label: string;
|
||||
secondaryLabel: string | null;
|
||||
confidence: DiscConfidence;
|
||||
/** سهم پاسخهاست، نه احتمال علمی. */
|
||||
shareNote: string;
|
||||
}
|
||||
|
||||
function emptyScores(): Record<DiscType, number> {
|
||||
return { D: 0, I: 0, S: 0, C: 0 };
|
||||
}
|
||||
|
||||
export function isDiscType(value: unknown): value is DiscType {
|
||||
return value === "D" || value === "I" || value === "S" || value === "C";
|
||||
}
|
||||
|
||||
export function scoreDisc(answers: DiscAnswers | null | undefined): DiscScoreResult {
|
||||
const scores = emptyScores();
|
||||
const known = new Set(DISC_QUESTIONS.map((q) => q.id));
|
||||
let validCount = 0;
|
||||
let skippedCount = 0;
|
||||
for (const question of DISC_QUESTIONS) {
|
||||
const value = answers?.[question.id];
|
||||
if (value == null || value === undefined) {
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (!known.has(question.id) || !isDiscType(value)) continue;
|
||||
scores[value] += 1;
|
||||
validCount += 1;
|
||||
}
|
||||
|
||||
const percents = emptyScores();
|
||||
if (validCount > 0) {
|
||||
for (const type of DISC_TYPES) {
|
||||
percents[type] = Math.round((scores[type] / validCount) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = DISC_TYPES.map((type) => ({ type, score: scores[type] })).sort(
|
||||
(a, b) => b.score - a.score || DISC_TYPES.indexOf(a.type) - DISC_TYPES.indexOf(b.type),
|
||||
);
|
||||
const top = ranked[0]?.score ?? 0;
|
||||
const reliable = validCount >= DISC_MIN_VALID_ANSWERS && top > 0;
|
||||
|
||||
if (!reliable) {
|
||||
return {
|
||||
scores,
|
||||
percents,
|
||||
validCount,
|
||||
skippedCount,
|
||||
reliable: false,
|
||||
primaryTypes: [],
|
||||
secondaryTypes: [],
|
||||
label: DISC_INSUFFICIENT_LABEL,
|
||||
secondaryLabel: null,
|
||||
confidence: "insufficient",
|
||||
shareNote: "با کمتر از چهار پاسخ معتبر، سهم پاسخها برای یک نتیجهٔ قابل اتکا کافی نیست.",
|
||||
};
|
||||
}
|
||||
|
||||
const primaryTypes = ranked.filter((row) => row.score === top).map((row) => row.type);
|
||||
const nextScore = ranked.find((row) => row.score < top && row.score > 0)?.score ?? 0;
|
||||
const secondaryTypes =
|
||||
nextScore > 0 ? ranked.filter((row) => row.score === nextScore).map((row) => row.type) : [];
|
||||
const label = primaryTypes.join("/");
|
||||
const secondaryLabel = secondaryTypes.length ? secondaryTypes.join("/") : null;
|
||||
const gap = top - (secondaryTypes.length ? nextScore : 0);
|
||||
const share = percents[primaryTypes[0]!] ?? 0;
|
||||
let confidence: DiscConfidence = "low";
|
||||
if (primaryTypes.length === 1 && gap >= 3 && share >= 50) confidence = "high";
|
||||
else if (primaryTypes.length === 1 && gap >= 2) confidence = "medium";
|
||||
|
||||
return {
|
||||
scores,
|
||||
percents,
|
||||
validCount,
|
||||
skippedCount,
|
||||
reliable: true,
|
||||
primaryTypes,
|
||||
secondaryTypes,
|
||||
label,
|
||||
secondaryLabel,
|
||||
confidence,
|
||||
shareNote: "درصدها فقط سهم پاسخهای همین پرسشنامه هستند و احتمال علمی یا دقت تشخیص نیستند.",
|
||||
};
|
||||
}
|
||||
|
||||
export function discGuideFor(types: DiscType[]): DiscGuide {
|
||||
const merged: DiscGuide = { strengths: [], negotiation: [], presentation: [], followUp: [] };
|
||||
const seen = new Set<string>();
|
||||
for (const type of types) {
|
||||
const guide = DISC_GUIDES[type];
|
||||
for (const key of ["strengths", "negotiation", "presentation", "followUp"] as const) {
|
||||
for (const line of guide[key]) {
|
||||
if (seen.has(line)) continue;
|
||||
seen.add(line);
|
||||
merged[key].push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function discProfileIsFresh(assessedAt: string | null | undefined, now = Date.now()): boolean {
|
||||
if (!assessedAt) return false;
|
||||
const at = new Date(assessedAt).getTime();
|
||||
if (!Number.isFinite(at)) return false;
|
||||
return now - at <= DISC_FRESH_DAYS * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
export const COPILOT_TOPICS = [
|
||||
"intro",
|
||||
"discovery",
|
||||
"pricing",
|
||||
"follow_up",
|
||||
"objection",
|
||||
"closing",
|
||||
"renewal",
|
||||
] as const;
|
||||
export type CopilotTopic = (typeof COPILOT_TOPICS)[number];
|
||||
|
||||
export const COPILOT_TOPIC_LABELS: Record<CopilotTopic, string> = {
|
||||
intro: "معرفی اولیه",
|
||||
discovery: "بررسی نیاز",
|
||||
pricing: "ارائه قیمت",
|
||||
follow_up: "پیگیری پیشنهاد",
|
||||
objection: "پاسخ به اعتراض",
|
||||
closing: "مذاکره نهایی",
|
||||
renewal: "تمدید یا فروش مجدد",
|
||||
};
|
||||
|
||||
export const COPILOT_GOALS = [
|
||||
"discover_need",
|
||||
"find_objection",
|
||||
"book_next",
|
||||
"fit_offer",
|
||||
"follow_price",
|
||||
"close",
|
||||
"keep_relation",
|
||||
] as const;
|
||||
export type CopilotGoal = (typeof COPILOT_GOALS)[number];
|
||||
|
||||
export const COPILOT_GOAL_LABELS: Record<CopilotGoal, string> = {
|
||||
discover_need: "کشف نیاز واقعی",
|
||||
find_objection: "شناخت علت مخالفت",
|
||||
book_next: "تعیین جلسه بعدی",
|
||||
fit_offer: "ارائه پیشنهاد مناسب",
|
||||
follow_price: "پیگیری قیمت",
|
||||
close: "نهاییکردن فروش",
|
||||
keep_relation: "حفظ ارتباط برای آینده",
|
||||
};
|
||||
|
||||
export const OBJECTION_CATEGORIES = [
|
||||
"price_high",
|
||||
"no_need",
|
||||
"need_time",
|
||||
"consult",
|
||||
"have_similar",
|
||||
"no_trust",
|
||||
"bad_timing",
|
||||
"no_time_to_review",
|
||||
"bad_past",
|
||||
"send_info",
|
||||
"not_interested",
|
||||
"unclear",
|
||||
] as const;
|
||||
export type ObjectionCategory = (typeof OBJECTION_CATEGORIES)[number];
|
||||
|
||||
export const OBJECTION_LABELS: Record<ObjectionCategory, string> = {
|
||||
price_high: "قیمت بالاست",
|
||||
no_need: "فعلاً نیاز ندارم",
|
||||
need_time: "باید فکر کنم",
|
||||
consult: "باید مشورت کنم",
|
||||
have_similar: "محصول مشابه دارم",
|
||||
no_trust: "به سیستم جدید اعتماد ندارم",
|
||||
bad_timing: "الان زمان مناسبی نیست",
|
||||
no_time_to_review: "فرصت بررسی ندارم",
|
||||
bad_past: "قبلاً تجربه ناموفق داشتهام",
|
||||
send_info: "لطفاً اطلاعات را ارسال کنید",
|
||||
not_interested: "فعلاً تمایلی به ادامه ندارم",
|
||||
unclear: "اعتراض هنوز روشن نیست",
|
||||
};
|
||||
|
||||
export const FOLLOW_UP_MESSAGE_KINDS = [
|
||||
"whatsapp_after_call",
|
||||
"sms",
|
||||
"price_followup",
|
||||
"send_proposal",
|
||||
"meeting",
|
||||
"after_demo",
|
||||
"hesitant",
|
||||
"keep_in_touch",
|
||||
] as const;
|
||||
export type FollowUpMessageKind = (typeof FOLLOW_UP_MESSAGE_KINDS)[number];
|
||||
|
||||
export const FOLLOW_UP_MESSAGE_LABELS: Record<FollowUpMessageKind, string> = {
|
||||
whatsapp_after_call: "واتساپ پس از تماس",
|
||||
sms: "پیامک کوتاه",
|
||||
price_followup: "پیگیری قیمت",
|
||||
send_proposal: "ارسال پیشنهاد",
|
||||
meeting: "هماهنگی جلسه",
|
||||
after_demo: "پیگیری پس از دمو",
|
||||
hesitant: "پیگیری مشتری مردد",
|
||||
keep_in_touch: "حفظ ارتباط برای آینده",
|
||||
};
|
||||
|
||||
export interface CopilotDiscContext {
|
||||
label: string;
|
||||
primaryTypes: DiscType[];
|
||||
reliable: boolean;
|
||||
assessedAt: string | null;
|
||||
fresh: boolean;
|
||||
used: boolean;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface CopilotAnalysis {
|
||||
customer_statement_summary: string;
|
||||
objection_category: ObjectionCategory | null;
|
||||
objection_label: string | null;
|
||||
possible_underlying_reasons: string[];
|
||||
evidence: string[];
|
||||
missing_information: string[];
|
||||
recommended_question: string;
|
||||
suggested_response: string;
|
||||
common_mistake: string;
|
||||
sales_strategy: string[];
|
||||
next_best_action: string;
|
||||
suggested_follow_up_date: string | null;
|
||||
disc_context: CopilotDiscContext | null;
|
||||
confidence_note: string;
|
||||
needs_more_input: boolean;
|
||||
source: "rules" | "rules+ai";
|
||||
}
|
||||
|
||||
export interface CopilotFacts {
|
||||
customerName?: string | null;
|
||||
businessType?: string | null;
|
||||
productName?: string | null;
|
||||
stageName?: string | null;
|
||||
/** فقط اگر در CRM ثبت شده باشد؛ هرگز ساخته نمیشود. */
|
||||
priceOffered?: string | null;
|
||||
lastAction?: string | null;
|
||||
disc?: {
|
||||
label: string;
|
||||
primaryTypes: DiscType[];
|
||||
reliable: boolean;
|
||||
assessedAt: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CopilotInput {
|
||||
topic: CopilotTopic;
|
||||
goal: CopilotGoal;
|
||||
customerStatement: string;
|
||||
behaviorNotes?: string | null;
|
||||
facts: CopilotFacts;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
interface ObjectionPlay {
|
||||
category: ObjectionCategory;
|
||||
patterns: RegExp[];
|
||||
reasons: string[];
|
||||
question: string;
|
||||
response: string;
|
||||
mistake: string;
|
||||
strategy: string;
|
||||
next: string;
|
||||
followUpDays: number | null;
|
||||
}
|
||||
|
||||
const OBJECTION_PLAYS: ObjectionPlay[] = [
|
||||
{
|
||||
category: "price_high",
|
||||
patterns: [/گرونه/, /گران/, /قیمت.{0,12}بالا/, /بودجه/, /تخفیف/, /ارزونتر/, /ارزانتر/],
|
||||
reasons: [
|
||||
"ممکن است قیمت نسبت به بودجه زیاد به نظر برسد.",
|
||||
"ممکن است ارزش تغییر هنوز برایش روشن نباشد.",
|
||||
"ممکن است هزینهٔ پنهان یا مقایسه با گزینهٔ دیگر در ذهن باشد.",
|
||||
],
|
||||
question: "اگر قیمت را کنار بگذاریم، خودِ راهکار چقدر به کار امروزتان میخورد؟",
|
||||
response:
|
||||
"متوجهام که عدد مهم است. قبل از حرف زدن دربارهٔ تخفیف، دوست دارم ببینم کدام بخش کار برایتان خروجی مشخص دارد تا مقایسه منصفانه باشد.",
|
||||
mistake: "تخفیف فوری دادن قبل از فهمیدن اینکه اعتراض واقعاً به قیمت است یا به ارزش.",
|
||||
strategy: "اگر اعتراض به هزینه است، هزینه و منفعت را فقط با اعداد ثبتشده مقایسه کنید.",
|
||||
next: "شفافکردن ارزش مورد انتظار، سپس در صورت وجود قیمت ثبتشده همان را مرور کنید.",
|
||||
followUpDays: 2,
|
||||
},
|
||||
{
|
||||
category: "no_need",
|
||||
patterns: [/نیاز ندار/, /لازم ندار/, /جواب میده/, /روش فعلی/, /لازم نیست/],
|
||||
reasons: [
|
||||
"ممکن است واقعاً نیازی نباشد.",
|
||||
"ممکن است ارزش تغییر هنوز دیده نشده باشد.",
|
||||
"ممکن است هزینهٔ عوض کردن روش فعلی بالا به نظر برسد.",
|
||||
],
|
||||
question:
|
||||
"کاملاً متوجهام. فقط برای اینکه بهتر شرایطتان را درک کنم، در روش فعلی بیشتر کدام بخش کار برایتان زمانبر است یا نیاز به پیگیری بیشتری دارد؟",
|
||||
response:
|
||||
"اگر روش فعلی واقعاً کارتان را راه میاندازد، اصراری به تغییر نیست. فقط اگر یک بخش از کار هنوز دستوپاگیر است، همان را جدا کنیم و ببینیم ارزش تغییر دارد یا نه.",
|
||||
mistake: "فرض کردن اینکه «نیاز ندارم» یعنی درِ فروش برای همیشه بسته است.",
|
||||
strategy: "ابتدا نیاز را دقیقتر بررسی کنید و بدون اثبات نیاز، به خرید سوق ندهید.",
|
||||
next: "یک سؤال کشف نیاز؛ اگر نیاز واقعی نبود، ارتباط را با اجازهٔ مشتری برای آینده حفظ کنید.",
|
||||
followUpDays: 7,
|
||||
},
|
||||
{
|
||||
category: "need_time",
|
||||
patterns: [/فکر کنم/, /فکر میکنم/, /بررسی کنم/, /بعداً/, /فرصت بده/],
|
||||
reasons: ["نیاز به زمان برای تصمیم دارد.", "ممکن است اطلاعات کم باشد.", "ممکن است نخواهد در لحظه متعهد شود."],
|
||||
question: "برای اینکه بررسیتان دقیق باشد، تا چه تاریخی و با کدام سؤال برگردم؟",
|
||||
response: "حق دارید فرصت بررسی داشته باشید. همان سؤالهایی که مانده را جدا میکنم و در تاریخی که خودتان بگویید پیگیری میکنم.",
|
||||
mistake: "فشار برای جواب همان لحظه.",
|
||||
strategy: "زمان مناسب پیگیری را با مشتری مشخص کنید.",
|
||||
next: "ثبت تاریخ پیگیری مورد توافق.",
|
||||
followUpDays: 3,
|
||||
},
|
||||
{
|
||||
category: "consult",
|
||||
patterns: [/مشورت/, /شریک/, /مدیر/, /باید بپرسم/, /تصمیم با/],
|
||||
reasons: ["تصمیم فردی نیست.", "باید معیار تصمیمگیرندهٔ دیگر را دانست."],
|
||||
question: "کسی که قرار است مشورت کنید بیشتر روی نتیجه، قیمت یا جزئیات حساس است؟",
|
||||
response: "منطقی است با شریک یا مدیرتان هماهنگ کنید. اگر بگویید معیار اصلی آنها چیست، همان را شفاف میفرستم تا جلسهٔ داخلیتان کوتاهتر شود.",
|
||||
mistake: "دور زدن تصمیمگیرنده یا اصرار به بستن فروش با کسی که اختیار ندارد.",
|
||||
strategy: "ابزار تصمیم را برای نفر بعدی آماده کنید، نه فشار روی واسطه.",
|
||||
next: "ارسال خلاصهٔ قابل ارائه به تصمیمگیرنده و هماهنگی پیگیری بعد از مشورت.",
|
||||
followUpDays: 3,
|
||||
},
|
||||
{
|
||||
category: "have_similar",
|
||||
patterns: [/مشابه/, /قبلاً داریم/, /نرمافزار دیگه/, /سیستم دیگه/, /در حال استفاده/],
|
||||
reasons: ["جایگزینی باید بهتر از وضع موجود باشد.", "ممکن است مقایسه هنوز منصفانه نباشد."],
|
||||
question: "در ابزاری که الان دارید، کدام کار هنوز دستی میماند یا دوبارهکاری دارد؟",
|
||||
response: "اگر ابزار فعلی بخش مهمی از کار را پوشش میدهد، تغییر فقط وقتی معنی دارد که یک درد مشخص را کم کند. همان تفاوت را با هم محدود کنیم.",
|
||||
mistake: "بدگویی از ابزار فعلی بدون دانستن کار واقعی مشتری.",
|
||||
strategy: "مقایسه را به یک درد مشخص محدود کنید.",
|
||||
next: "کشف شکاف ابزار فعلی؛ اگر شکافی نیست، پیگیری را متوقف یا خیلی سبک کنید.",
|
||||
followUpDays: 5,
|
||||
},
|
||||
{
|
||||
category: "no_trust",
|
||||
patterns: [/اعتماد/, /مطمئن نیستم/, /نگرانم/, /ریسک/],
|
||||
reasons: ["ریسک تغییر یا اجرای بد را بالا میبیند.", "تعهد پشتیبانی برایش مبهم است."],
|
||||
question: "کدام بخش بیشتر نگرانتان میکند: راهاندازی، داده، یا پشتیبانی بعد از شروع؟",
|
||||
response: "نگرانی بهجا است. همان بخش را مشخص کنیم و فقط دربارهٔ تعهدی حرف بزنم که واقعاً در شرایط همکاری هست، نه قول کلی.",
|
||||
mistake: "اطمینانبخشی شعاری بدون محدودهٔ تعهد.",
|
||||
strategy: "تعهد، محدودیت و پشتیبانی را شفاف بگویید.",
|
||||
next: "پاسخ به همان نگرانی مشخص، سپس تصمیم دربارهٔ قدم بعدی.",
|
||||
followUpDays: 2,
|
||||
},
|
||||
{
|
||||
category: "bad_timing",
|
||||
patterns: [/زمان مناسب نیست/, /سرت شلوغ/, /سرم شلوغ/, /این ماه نه/, /فصل بعد/, /سال بعد/],
|
||||
reasons: ["اولویت فعلی جای دیگری است.", "ممکن است بهانهٔ مؤدبانه برای توقف باشد."],
|
||||
question: "اگر زمان مناسب نیست، چه اتفاقی باید بیفتد که برگشتن به این موضوع برایتان معنی داشته باشد؟",
|
||||
response: "اگر الان اولویت نیست، اصرار نمیکنم. فقط مشخص کنیم پیگیری بعدی را میخواهید یا پرونده را همینجا ببندیم.",
|
||||
mistake: "تقویم پیگیری شلوغ بدون اجازهٔ مشتری.",
|
||||
strategy: "یا تاریخ واقعی بگیرید یا پیگیری را متوقف کنید.",
|
||||
next: "ثبت اجازهٔ پیگیری بعدی یا توقف محترمانه.",
|
||||
followUpDays: 14,
|
||||
},
|
||||
{
|
||||
category: "no_time_to_review",
|
||||
patterns: [/وقت ندار/, /فرصت ندار/, /فرصت بررسی/, /سرم شلوغ است/],
|
||||
reasons: ["بار کاری بالاست.", "ممکن است بررسی را به تعویق بیندازد چون اولویت ندارد."],
|
||||
question: "اگر یک خلاصهٔ خیلی کوتاه بفرستم، چه روزی پنج دقیقه برای نگاه کردنش دارید؟",
|
||||
response: "وقتتان را با توضیح طولانی نمیگیرم. اگر بخواهید یک جمعبندی کوتاه میفرستم و خودتان زمان نگاه کردن را میگویید.",
|
||||
mistake: "فرستادن پروندهٔ سنگین وقتی مشتری گفته وقت ندارد.",
|
||||
strategy: "حجم را کم کنید و زمان را از مشتری بگیرید.",
|
||||
next: "ارسال خلاصهٔ کوتاه فقط در صورت تمایل، با تاریخ مشخص.",
|
||||
followUpDays: 4,
|
||||
},
|
||||
{
|
||||
category: "bad_past",
|
||||
patterns: [/تجربه بد/, /دفعه قبل/, /قبلاً شکست/, /ناموفق/, /سوختم/],
|
||||
reasons: ["حافظهٔ یک اجرای بد هنوز فعال است.", "باید فرق این همکاری با تجربهٔ قبلی روشن شود."],
|
||||
question: "در تجربهٔ قبلی دقیقاً کدام بخش خراب شد: قول فروش، اجرا، یا پشتیبانی؟",
|
||||
response: "حق دارید با تجربهٔ قبلی محتاط باشید. اگر بگویید کدام بخش خراب شده، فقط همان را با شرایط واقعی این همکاری مقایسه میکنم و چیزی را که تضمین نیست وعده نمیدهم.",
|
||||
mistake: "گفتن اینکه «این بار فرق دارد» بدون شنیدن جزئیات تجربهٔ قبلی.",
|
||||
strategy: "علت شکست قبلی را بشنوید و فقط تعهد واقعی را جدا کنید.",
|
||||
next: "ثبت علت تجربهٔ قبلی و پاسخ محدود به همان علت.",
|
||||
followUpDays: 3,
|
||||
},
|
||||
{
|
||||
category: "send_info",
|
||||
patterns: [/بفرست/, /ارسال کن/, /اطلاعات بده/, /کاتالوگ/, /پیام بده/],
|
||||
reasons: ["ممکن است واقعاً بخواهد مطالعه کند.", "ممکن است بخواهد مکالمه را کوتاه کند."],
|
||||
question: "کدام بخش را بفرستم که به تصمیمتان کمک کند: محدودهٔ کار، شرایط قیمت ثبتشده، یا مراحل اجرا؟",
|
||||
response: "حتماً. بهجای یک فایل کلی، همان بخشی را میفرستم که گفتید برایتان مهم است و بعد میپرسم کی به آن نگاه میکنید.",
|
||||
mistake: "فرستادن معرفی عمومی و قطع کردن کشف نیاز.",
|
||||
strategy: "محتوا را به سؤال باز مشتری محدود کنید.",
|
||||
next: "ارسال اطلاعات درخواستی و ثبت زمان بازگشت.",
|
||||
followUpDays: 2,
|
||||
},
|
||||
{
|
||||
category: "not_interested",
|
||||
patterns: [/تمایل ندار/, /نمیخوام/, /نمیخوام/, /منصرف/, /پیگیری نکن/, /دیگر زنگ نزن/, /دیگه زنگ نزن/],
|
||||
reasons: ["تمایل به ادامه نیست.", "باید احترام به توقف رعایت شود."],
|
||||
question: "اگر مایل باشید فقط دلیل اصلی را در یک جمله بگویید تا دیگر مزاحم همین موضوع نشوم.",
|
||||
response: "حتماً پیگیری را متوقف میکنم. اگر روزی خودتان خواستید برگردیم، در خدمتم.",
|
||||
mistake: "ادامهٔ تماس بعد از اعلام عدم تمایل.",
|
||||
strategy: "پیگیری را متوقف کنید و فقط در صورت اجازه، دلیل را ثبت کنید.",
|
||||
next: "توقف پیگیری.",
|
||||
followUpDays: null,
|
||||
},
|
||||
];
|
||||
|
||||
const DISC_OPENERS: Record<DiscType, string> = {
|
||||
D: "کوتاه میگویم: ",
|
||||
I: "حق دارید اینطور احساس کنید. ",
|
||||
S: "عجلهای نیست. ",
|
||||
C: "فقط بر اساس چیزی که ثبت شده: ",
|
||||
};
|
||||
|
||||
function subjectName(facts: CopilotFacts): string {
|
||||
const name = facts.productName?.trim() || facts.businessType?.trim();
|
||||
return name || "موضوع همین گفتگو";
|
||||
}
|
||||
|
||||
function customerName(facts: CopilotFacts): string {
|
||||
return facts.customerName?.trim() || "مشتری";
|
||||
}
|
||||
|
||||
function discContext(facts: CopilotFacts, now: Date): CopilotDiscContext | null {
|
||||
const disc = facts.disc;
|
||||
if (!disc) return null;
|
||||
const fresh = disc.reliable && discProfileIsFresh(disc.assessedAt, now.getTime());
|
||||
const used = fresh && disc.primaryTypes.length > 0;
|
||||
return {
|
||||
label: disc.label,
|
||||
primaryTypes: disc.primaryTypes,
|
||||
reliable: disc.reliable,
|
||||
assessedAt: disc.assessedAt,
|
||||
fresh,
|
||||
used,
|
||||
note: used
|
||||
? "تیپ DISC فقط راهنمای لحن است و تشخیص قطعی شخصیت نیست."
|
||||
: "پروفایل DISC معتبر و تازه نیست؛ از روی این جمله تیپ تعیین نمیشود.",
|
||||
};
|
||||
}
|
||||
|
||||
function matchObjection(text: string): ObjectionPlay | null {
|
||||
for (const play of OBJECTION_PLAYS) {
|
||||
if (play.patterns.some((pattern) => pattern.test(text))) return play;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stageHint(stageName: string | null | undefined): string | null {
|
||||
const stage = (stageName ?? "").trim();
|
||||
if (!stage) return null;
|
||||
if (/از دست|ناموفق|باخت/.test(stage)) {
|
||||
return "فروش ازدسترفته: علت واقعی را ثبت کنید و فقط با تمایل مشتری به پیگیری بعدی فکر کنید.";
|
||||
}
|
||||
if (/نهایی|بستن|قرارداد/.test(stage)) {
|
||||
return "نزدیک نهاییشدن: مانع باقیمانده را نام ببرید و قدم بعدی را تاریخدار کنید.";
|
||||
}
|
||||
if (/مذاکر/.test(stage)) return "مذاکره: اعتراض را دستهبندی کنید و شرایط را شفاف کنید.";
|
||||
if (/پیشنهاد|قیمت/.test(stage)) return "پیشنهاد ارسال شده: ابهام باقیمانده را بپرسید، قیمت جدید نسازید.";
|
||||
if (/نیاز/.test(stage)) return "نیاز شناسایی شده: راهکار را به همان نیاز محدود کنید.";
|
||||
if (/سرنخ|جدید|تماس اول/.test(stage)) return "سرنخ جدید: کشف نیاز و صلاحیتسنجی مقدم بر ارائه است.";
|
||||
return null;
|
||||
}
|
||||
|
||||
function addDays(from: Date, days: number): string {
|
||||
const next = new Date(from.getTime());
|
||||
next.setDate(next.getDate() + days);
|
||||
const y = next.getFullYear();
|
||||
const m = String(next.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(next.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export function analyzeCopilot(input: CopilotInput): CopilotAnalysis {
|
||||
const now = input.now ?? new Date();
|
||||
const statement = input.customerStatement.replace(/\s+/g, " ").trim();
|
||||
const notes = input.behaviorNotes?.trim() || "";
|
||||
const facts = input.facts;
|
||||
const disc = discContext(facts, now);
|
||||
const evidence = [statement, notes].filter(Boolean);
|
||||
const stage = stageHint(facts.stageName);
|
||||
const primary = disc?.used ? disc.primaryTypes[0]! : null;
|
||||
|
||||
if (statement.length < 8) {
|
||||
return {
|
||||
customer_statement_summary: "حرف دقیق مشتری هنوز ثبت نشده است.",
|
||||
objection_category: null,
|
||||
objection_label: null,
|
||||
possible_underlying_reasons: [],
|
||||
evidence,
|
||||
missing_information: ["متن حرف، اعتراض یا دغدغهٔ مشتری"],
|
||||
recommended_question: "ممکن است همان جملهای که گفتید را دقیقتر بگویید تا ببینم مانع اصلی چیست؟",
|
||||
suggested_response: "",
|
||||
common_mistake: "ساختن پاسخ تبلیغاتی وقتی هنوز حرف مشتری ثبت نشده است.",
|
||||
sales_strategy: ["تا شواهد مکالمه کم است، سؤال تکمیلی بپرسید و نتیجهٔ قطعی اعلام نکنید."],
|
||||
next_best_action: "ثبت حرف مشتری",
|
||||
suggested_follow_up_date: null,
|
||||
disc_context: disc,
|
||||
confidence_note: "اطلاعات کافی نیست. دستیار بهجای پاسخ قطعی، سؤال تکمیلی پیشنهاد میدهد.",
|
||||
needs_more_input: true,
|
||||
source: "rules",
|
||||
};
|
||||
}
|
||||
|
||||
const play = matchObjection(statement);
|
||||
const missing: string[] = [];
|
||||
if (!facts.productName?.trim()) missing.push("نام محصول یا خدمت مورد مذاکره در پرونده خالی است");
|
||||
if (input.topic === "pricing" && !facts.priceOffered?.trim()) {
|
||||
missing.push("قیمت یا شرایط ثبتشده در پرونده نیست و نباید ساخته شود");
|
||||
}
|
||||
if (!facts.stageName?.trim()) missing.push("مرحلهٔ قیف فروش مشخص نیست");
|
||||
|
||||
const reasons = play
|
||||
? play.reasons
|
||||
: ["معنی این جمله هنوز چندوجهی است و بدون سؤال بعدی نباید یک علت را قطعی دانست."];
|
||||
const category = play?.category ?? "unclear";
|
||||
const question = play?.question ?? "دقیقاً کدام بخش از پیشنهاد برایتان مبهم یا غیرضروری است؟";
|
||||
let response = play
|
||||
? play.response
|
||||
: "برای اینکه پاسخ کلی ندهم، دوست دارم همان بخش از حرفتان را که مانع اصلی است جدا کنیم.";
|
||||
if (primary) response = `${DISC_OPENERS[primary]}${response}`;
|
||||
if (facts.priceOffered?.trim() && category === "price_high") {
|
||||
response += ` قیمت ثبتشده در پرونده ${facts.priceOffered.trim()} است و عدد دیگری ساخته نمیشود.`;
|
||||
}
|
||||
if (category === "price_high" && !facts.priceOffered?.trim()) {
|
||||
response += " چون قیمتی در پرونده ثبت نشده، عدد یا تخفیف جدیدی پیشنهاد نمیشود.";
|
||||
}
|
||||
|
||||
const strategy = [
|
||||
play?.strategy ?? "قبل از پاسخ قطعی، اطلاعات نامشخص را با یک سؤال بپرسید.",
|
||||
`هدف این تماس: ${COPILOT_GOAL_LABELS[input.goal]}.`,
|
||||
];
|
||||
if (stage) strategy.push(stage);
|
||||
if (primary) {
|
||||
const guide = DISC_GUIDES[primary].negotiation[0];
|
||||
if (guide) strategy.push(`با توجه به تیپ احتمالی ${primary}: ${guide}`);
|
||||
}
|
||||
if (notes) strategy.push("نشانههای رفتاری ثبتشده را جدا از تیپ شخصیتی ببینید و با رفتار واقعی تطبیق دهید.");
|
||||
|
||||
const summary = play
|
||||
? `مشتری چیزی نزدیک به «${OBJECTION_LABELS[play.category]}» گفته است: ${statement}`
|
||||
: `خلاصهٔ حرف مشتری: ${statement}`;
|
||||
|
||||
return {
|
||||
customer_statement_summary: summary,
|
||||
objection_category: category,
|
||||
objection_label: OBJECTION_LABELS[category],
|
||||
possible_underlying_reasons: reasons,
|
||||
evidence,
|
||||
missing_information: missing,
|
||||
recommended_question: question,
|
||||
suggested_response: response,
|
||||
common_mistake: play?.mistake ?? "اعلام یک معنی قطعی برای جملهای که هنوز مبهم است.",
|
||||
sales_strategy: strategy,
|
||||
next_best_action: play?.next ?? "یک سؤال تکمیلی برای روشن شدن مانع.",
|
||||
suggested_follow_up_date: play?.followUpDays ? addDays(now, play.followUpDays) : null,
|
||||
disc_context: disc,
|
||||
confidence_note: play
|
||||
? "این دستهبندی احتمالی است و تا وقتی مشتری علت را تأیید نکرده، حقیقت قطعی پرونده نیست."
|
||||
: "اعتراض در دستههای شناختهشده جا نگرفت. پاسخ قطعی داده نشد.",
|
||||
needs_more_input: !play,
|
||||
source: "rules",
|
||||
};
|
||||
}
|
||||
|
||||
export function followUpMessage(kind: FollowUpMessageKind, facts: CopilotFacts, analysis: CopilotAnalysis): string {
|
||||
const name = customerName(facts);
|
||||
const subject = subjectName(facts);
|
||||
const price = facts.priceOffered?.trim();
|
||||
const disc = analysis.disc_context?.used ? analysis.disc_context.primaryTypes[0] : null;
|
||||
const warm = disc === "I" || disc === "S";
|
||||
const hello = warm ? `${name} جان سلام` : `${name} سلام`;
|
||||
const priceLine = price
|
||||
? `قیمت ثبتشده همان ${price} است.`
|
||||
: "عدد قیمت را اینجا تکرار نمیکنم چون در این گفتگو قیمت تازهای ثبت نشده است.";
|
||||
|
||||
switch (kind) {
|
||||
case "sms":
|
||||
return `${hello}. پیرو صحبت دربارهٔ ${subject}، هر وقت جمعبندیتان آماده بود همینجا بفرمایید.`;
|
||||
case "price_followup":
|
||||
return `${hello}. خواستم پیگیری قیمت ${subject} را کوتاه بپرسم. ${priceLine} اگر سؤال مشخصی مانده بگویید تا همان را جواب بدهم.`;
|
||||
case "send_proposal":
|
||||
return `${hello}. خلاصهٔ ${subject} را میفرستم تا با آرامش ببینید. اگر بخشی اضافی است بگویید تا همان را حذف کنم.`;
|
||||
case "meeting":
|
||||
return `${hello}. برای ادامهٔ صحبت دربارهٔ ${subject} چه زمانی برایتان مناسب است؟`;
|
||||
case "after_demo":
|
||||
return `${hello}. بعد از دموی ${subject} کدام بخش برایتان روشن شد و کدام سؤال هنوز باز است؟`;
|
||||
case "hesitant":
|
||||
return `${hello}. اگر هنوز برای ${subject} مردد هستید، لازم نیست عجله کنید. فقط بگویید مانع اصلی زمان است، قیمت، یا خودِ نیاز.`;
|
||||
case "keep_in_touch":
|
||||
return `${hello}. اگر الان قصد ادامهٔ ${subject} را ندارید، مزاحم نمیشوم. هر زمان خودتان خواستید در خدمتم.`;
|
||||
default:
|
||||
return `${hello}. پیرو مکالمه دربارهٔ ${subject}: ${analysis.customer_statement_summary.slice(0, 180)}. سؤال بعدی من این است: ${analysis.recommended_question}`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiscAssessmentDto {
|
||||
id: string;
|
||||
personId: string;
|
||||
assessorId: string;
|
||||
assessorName: string;
|
||||
answers: DiscAnswers;
|
||||
scores: Record<DiscType, number>;
|
||||
percents: Record<DiscType, number>;
|
||||
validCount: number;
|
||||
primaryTypes: DiscType[];
|
||||
secondaryTypes: DiscType[];
|
||||
label: string;
|
||||
secondaryLabel: string | null;
|
||||
reliable: boolean;
|
||||
confidence: DiscConfidence;
|
||||
note: string | null;
|
||||
status: "confirmed";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DiscDistributionRow {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SalesCopilotSessionDto {
|
||||
id: string;
|
||||
personId: string | null;
|
||||
leadId: string | null;
|
||||
dealId: string | null;
|
||||
userId: string;
|
||||
userName: string;
|
||||
topic: CopilotTopic;
|
||||
goal: CopilotGoal;
|
||||
customerStatement: string;
|
||||
behaviorNotes: string | null;
|
||||
analysis: CopilotAnalysis;
|
||||
confirmedResponse: string | null;
|
||||
nextAction: string | null;
|
||||
followUpAt: string | null;
|
||||
callOutcome: string | null;
|
||||
agentNote: string | null;
|
||||
status: "confirmed" | "dismissed";
|
||||
messageDraft: string | null;
|
||||
activityId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -157,7 +157,8 @@ export type OrgModuleKey =
|
||||
| "routines"
|
||||
| "gamification"
|
||||
| "prior_data_archive"
|
||||
| "treasury";
|
||||
| "treasury"
|
||||
| "behavior";
|
||||
|
||||
export const ORG_MODULE_KEYS: OrgModuleKey[] = [
|
||||
"customers",
|
||||
@@ -200,6 +201,7 @@ export const ORG_MODULE_KEYS: OrgModuleKey[] = [
|
||||
"gamification",
|
||||
"prior_data_archive",
|
||||
"treasury",
|
||||
"behavior",
|
||||
];
|
||||
|
||||
export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
|
||||
@@ -243,6 +245,7 @@ export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
|
||||
gamification: "باشگاه امتیازات",
|
||||
prior_data_archive: "بایگانی اطلاعات قبل",
|
||||
treasury: "مدیریت مالی",
|
||||
behavior: "شخصیتشناسی و دستیار فروش",
|
||||
};
|
||||
|
||||
export const ORG_MODULE_GROUPS: Array<{
|
||||
@@ -255,6 +258,7 @@ export const ORG_MODULE_GROUPS: Array<{
|
||||
title: "فروش",
|
||||
keys: [
|
||||
"pipeline",
|
||||
"behavior",
|
||||
"products",
|
||||
"invoices",
|
||||
"quotes",
|
||||
@@ -442,7 +446,8 @@ export type RolePermissionKey =
|
||||
| "conversations"
|
||||
| "conversations_groups"
|
||||
| "treasury"
|
||||
| "treasury_payroll";
|
||||
| "treasury_payroll"
|
||||
| "behavior";
|
||||
|
||||
export const ROLE_PERMISSION_ITEMS: Array<{
|
||||
key: RolePermissionKey;
|
||||
@@ -460,6 +465,7 @@ export const ROLE_PERMISSION_ITEMS: Array<{
|
||||
{ key: "conversations_groups", label: "ساخت گروه گفتگو" },
|
||||
{ key: "treasury", label: "مدیریت مالی" },
|
||||
{ key: "treasury_payroll", label: "حقوق و دستمزد" },
|
||||
{ key: "behavior", label: "شخصیتشناسی و دستیار فروش" },
|
||||
];
|
||||
|
||||
export interface OrgRoleMemberDto {
|
||||
@@ -904,6 +910,10 @@ export interface PersonDto {
|
||||
customFields: Record<string, unknown>;
|
||||
loyaltyPoints: number;
|
||||
loyaltyTier: LoyaltyTierSummaryDto | null;
|
||||
/** تیپ غالب یا ترکیبی تأییدشده؛ بدون مجوز رفتار خالی برمیگردد */
|
||||
discLabel: string | null;
|
||||
discReliable: boolean;
|
||||
discAssessedAt: string | null;
|
||||
company: CompanySummaryDto | null;
|
||||
owner: UserSummaryDto | null;
|
||||
createdAt: string;
|
||||
@@ -5725,3 +5735,4 @@ export interface TreasuryDashboardDto {
|
||||
/* ───────── باشگاه امتیازات (گیمیفیکیشن کارکنان) ───────── */
|
||||
|
||||
export * from "./gamification";
|
||||
export * from "./behavior";
|
||||
|
||||
Reference in New Issue
Block a user