المشروع ٠٧ · هندسة نظام CRM
نظام CRM صممته لوكالة سيارات، وكيف أغلقت ثغراته.
ثلاث طبقات، كل واحدة تقول ما هي: كيف كان نظامهم، وما صممته، وكيف أبنيه اليوم.
الكلام البسيط أولاً، ثم مخططات تستكشفها، وأداتان تفاعليتان، والكود خلف إعادة التصميم.
- مصادر للعملاء
- ٧
- أقسام
- ٦
- مجموعات تصنيف
- ٤
- ثغرة أُغلقت
- ١١
بقلم ماجد الأندجاني · آخر تحديث ٢ نوفمبر ٢٠٢٦
في هذه الصفحة
السياق
العملاء يصلون من سبع قنوات، ويتسربون بين ستة أقسام.
في ٢٠٢٤ عملت محللاً للتسويق في وكالة سيارات. كان العملاء يصلون إليها عبر الإعلانات، والموقع، ووسائل التواصل، والرسائل النصية، والبريد، والإعلانات الخارجية، والفعاليات، ومركز الاتصال. وكانت ستة أقسام تحتاج هؤلاء العملاء: المبيعات الهاتفية، وتجربة القيادة، وما بعد البيع، ومركز الاتصال، ومبيعات الأساطيل، والتسويق.
لم يكن بينها رابط. العملاء يُنسخون يدوياً، ويُحوَّلون بالبريد، ويُتابَعون في جدول بيانات، ويُعتبرون مفقودين بعد بضع مكالمات في عصر يوم واحد. ولم يكن أحد يستطيع أن يقول أي حملة باعت سيارة. رسمت كيف كان النظام يعمل، ثم صممت نظام CRM كاملاً. هذه الصفحة تعرض الاثنين، ثم الجزء الذي يهمني أكثر: أين قصّر تصميمي الأول، وكيف أغلق كل ثغرة فيه اليوم.
الطبقة ١ · ما وجدته
لقطتان لحركة العميل قبل إعادة التصميم.
الصناديق المتقطعة كانت تُنجز يدوياً. اختر أي صندوق لتقرأ ما كان يحدث فيه. وأسماء الأدوات معمّمة.
الإعلانات
الحملات ترسل الناس إلى الموقع ووسائل التواصل ومركز الاتصال، دون طريقة لمعرفة الحملة التي جاء منها العميل لاحقاً.
الموقع
النماذج تصل إلى فريق التسويق، لا إلى القسم الذي يحتاجها.
وسائل التواصل
الرسائل ونماذج العملاء تُجمع يدوياً وتمر عبر الوكالة الإعلانية.
مركز الاتصال
المتصلون يُسجَّلون في أداة استقبال منفصلة.
فريق التسويق، يدوياً
يستخرج كل عميل، ويرتبه، ويقرر يدوياً أي قسم يستلمه.
الوكالة الإعلانية
تمرر عملاء وسائل التواصل إلى التسويق، فيضاف تسليم آخر قبل أن يتصل أحد.
أداة الاستقبال
تسجل المكالمات، ثم تُنقل إلى نظام الوكالة يدوياً.
التحويل بالبريد
كل قسم يستلم عملاءه كرسائل بريد. لا شيء يتتبع هل فُتحت الرسالة، أو اتُّصل بالعميل، أو نُسي.
نظام الوكالة
نظام إدارة الوكالة يحمل البيع والصيانة، لكنه لا يعرف شيئاً عن العميل قبل البيع.
ما بعد البيع
تصلها طلبات الصيانة بالبريد.
الأساطيل
تصلها استفسارات الشركات بالبريد.
المبيعات الهاتفية
تتصل بكل عميل، فيصبح ساخناً أو مفقوداً. والساخن يكمل إلى نظام الوكالة، ثم فرع أو بنك، ثم الدفع، ثم موعد، ثم التسليم.
الإعلانات
صارت تشمل الفعاليات أيضاً، وما زالت دون تتبع للحملة التي جلبت كل عميل.
الفعاليات
زوار الأجنحة يعبئون نماذج.
الموقع
نماذج طلب السعر، والتواصل، والعروض، والاستفسار العام.
وسائل التواصل
نماذج العملاء، وعروض توجه الناس إلى الموقع.
مركز الاتصال
ما زال يُسجَّل في أداة الاستقبال.
النماذج، تُعالج يدوياً
بيانات النماذج تُنقل إلى أداة المهام يدوياً.
أداة الاستقبال
تغذي المبيعات الهاتفية مباشرة، خارج أداة المهام.
أداة المهام
المحور الجديد. يرسل العملاء إلى الأساطيل والمبيعات، لكنه لم يكن متصلاً بما بعد البيع أصلاً، فبقيت طلبات الصيانة تنتقل بالبريد.
ما بعد البيع
الطلبات تصل بالبريد في جداول، تُؤكد، ثم تُكتب في نظام الوكالة.
الأساطيل
تستلم العملاء من أداة المهام ومن العروض.
المبيعات الهاتفية
تعمل على قائمة انتظار: جديد، ثم يُوزَّع على الفريق، ثم تواصل، ثم مهتم أو غير مهتم أو لم يرد.
نظام الوكالة
ما زال يُغذّى يدوياً.
جدول البيانات ومحادثة الفريق
العملاء يخرجون من الأداة إلى جدول مشترك ومحادثة فريق. ويُتصل بالعميل ثلاث مرات في اليوم نفسه، وإن لم يرد يُعتبر مفقوداً.
التسليم اليدوي: كل عميل يمر بشخص واحد على الأقل ينسخه إلى مكان آخر.
لا مصدر: لا يمكن ربط أي حملة ببيع، فكانت الميزانيات تُحدد بالانطباع.
لا عميل واحد: الشخص نفسه قد يكون في أداة المهام وجدول البيانات ونظام الوكالة كثلاثة أشخاص مختلفين.
خسارة مبكرة: ثلاث مكالمات في عصر يوم واحد كانت تقرر أن العميل ضاع.
ما بعد البيع خارج النظام: طلبات الصيانة لم تدخل الأداة المشتركة أبداً.
الطبقة ٢ · التصميم
مدخل واحد، وموزّع واحد، وست قوائم للأقسام، ولا شيء يضيع في النهاية.
كل قناة تحمل رابط UTM، فيعرف كل عميل من أين جاء. وكل عميل يدخل مدخلاً واحداً، ويُفحص هل هو عميل سابق، ثم يُوجَّه إلى قائمة القسم الذي يحتاجه. وكل قسم يعالج قائمته بمحرك الحالات نفسه، وكل نهاية تصب في مستودع بيانات، والمبيعات وما بعد البيع يغذيان بعضهما عبر البيع الإضافي والبيع المتقاطع.
الفعاليات
زوار الأجنحة يسجلون في نماذج تدخل المدخل نفسه.
الرسائل النصية
العروض تحمل رابط UTM إلى الموقع، فتُعرف الزيارة من رسالتها. والخطوط المتقطعة روابط UTM.
الإعلانات الخارجية
اللوحات تشير إلى روابط متتبعة أيضاً.
الموقع
نماذج العروض وطلب السعر وتجربة القيادة. ونموذج التجربة يتيح لمدير الفرع التحكم بالتاريخ والوقت والسيارة.
البريد
رسائل الحملات ترتبط بالموقع عبر UTM.
وسائل التواصل
نماذج العملاء تدخل المدخل مباشرة، والعروض ترتبط بالموقع.
مركز الاتصال
المكالمات تُسجَّل في النماذج نفسها، ويُختار موعدها وتاريخها من المواعيد المتاحة فعلاً.
مدخل واحد
كل البيانات تصل إلى نظام واحد، ومعها مصدرها.
فحص العميل
هل هذا عميل نعرفه؟ إن كان كذلك ينضم الطلب إلى سجله، وإلا يُنشأ سجل جديد.
الموزّع
كل طلب يذهب إلى قائمة القسم الذي يحتاجه، بمتطلبات كل قسم.
المبيعات الهاتفية
طلبات السعر والعروض. انظر مسارات الأقسام أدناه.
تجربة القيادة
حجوزات لكل فرع، وتُسلَّم للمبيعات بعد التجربة.
ما بعد البيع
حجوزات الصيانة، وأسئلة القطع والخدمات.
مركز الاتصال
الاستفسارات العامة والشكاوى.
الأساطيل
استفسارات الشركات. ومسارها التفصيلي بقي مفتوحاً في تصميمي الأول.
التسويق
التواصل مع العملاء المحتملين، وبناء قاعدة عملاء واحدة، وتنظيم الردود على الشركات. ومساره التفصيلي بقي مفتوحاً في تصميمي الأول.
البيع الإضافي والمتقاطع
البيع يقود إلى الصيانة، والصيانة تقود إلى البيع التالي.
مستودع البيانات
كل نهاية، بيع أو خدمة أو خسارة أو إغلاق، تُحفظ للتحليل، فتُقيَّم الحملات والفرق بالبيانات.
الطبقة ٢ · مسارات الأقسام
كيف يتحرك العميل داخل كل قسم.
قائمة المبيعات
العملاء الموجهون للمبيعات ينتظرون هنا.
جديد، ثم يُسند
النظام يعلّم العميل جديداً ويوزع القائمة على الفريق.
التواصل
الموظف يتصل. وإن لم يرد العميل يعود إلى قائمة الانتظار ويُحاوَل معه مجدداً حتى ثلاث مرات، وبعد الثالثة ينضم إلى المجموعة الباردة.
الإجراء التالي
مهتم، أو مهتم جزئياً، أو غير مهتم، أو لم يرد. والمهتم يُصنَّف بحسب موعد شرائه.
ساخن
جاهز للشراء. يبقى العميل مع الموظف نفسه ١٥ يوماً حتى تكتمل الإجراءات.
دافئ
سيشتري خلال الأشهر القادمة. والعميل الساخن المعلّق يعود إلى هنا أيضاً.
بارد
من سيشتري لاحقاً، والمهتم جزئياً، ومن لم يُوصل إليه بعد ثلاث محاولات.
مفقود
غير مهتم. وتبقى المجموعة محفوظة، لا تُحذف.
البيع
بعد اكتمال الإجراءات، يُسجَّل البيع في نظام الوكالة، ثم موعد التسليم.
تسجيل السبب
لماذا لم يشترِ العميل، لتحسين الخدمة ومعالجة ما يُبعد العملاء.
موعد المتابعة
العملاء الدافئون والباردون يعودون للتواصل في موعد متابعتهم التالي.
قائمة تجربة القيادة
طلبات من الموقع أو مركز الاتصال، مرتبة حسب الفرع.
مدير تجربة القيادة بالفرع
يتحكم بالسيارة والتاريخ والوقت. وهي مترابطة، فأي تغيير يتحدث في كل مكان مرة واحدة.
التأكيد مع العميل
إن أراد العميل تغييراً يعود للمدير، وإلا يُؤكَّد الحجز.
التذكير
رسالة نصية تذكّر العميل قبل التجربة.
الوصول
يصل العميل إلى الفرع وتُتحقق بياناته.
تمت
تمت التجربة.
أُلغيت
تُسجَّل وتُغلق. وطلب التغيير يعود إلى مدير الفرع.
إلى المبيعات في اليوم التالي
بعد التجربة بيوم، ينتقل العميل تلقائياً إلى المبيعات ويُصنَّف ساخناً أو دافئاً أو بارداً.
قائمة ما بعد البيع
حجوزات الصيانة، وأسئلة القطع والخدمات.
حجز موعد
حين يُحجز الموعد، يُغلق في كل مكان مرة واحدة.
التذكير
رسالة نصية تذكّر العميل بالزيارة.
مستشار الخدمة
له صلاحية كاملة لإضافة المواعيد أو تغييرها، وكل تغيير يحدّث الحجز تلقائياً.
الوصول
حجوزات اليوم مرتبة حسب التاريخ والوقت.
حضر
لا يُؤكَّد إلا بعد وصول العميل وتسجيله في نظام الوكالة.
لم يحضر
يُعلَّم حين يمر الموعد دون حضور العميل. وطلب التغيير يعود إلى المستشار.
اكتمل
تُغلق الزيارة.
أسئلة القطع والخدمات
يتولاها فريق ما بعد البيع داخل مركز الاتصال. ومسارها التفصيلي بقي مفتوحاً في تصميمي الأول.
قائمة مركز الاتصال
كل مكالمة تصل هنا أولاً.
فحص العميل
محطة تفحص هل العميل موجود. إن كان موجوداً يُختار نوع القسم وتذهب بياناته إليه مباشرة، وإلا يُسجَّل أولاً. والاستفسارات والشكاوى تبقى عند مركز الاتصال.
إلى التسويق
تنتقل تلقائياً إلى قائمة التسويق.
إلى ما بعد البيع
طلب الحجز ينتقل إلى قائمة ما بعد البيع.
إلى التجربة
ينتقل إلى قائمة تجربة القيادة.
إلى المبيعات
ينتقل إلى قائمة المبيعات.
استفسار عام
يرد عليه مركز الاتصال، عبر القناة التي استخدمها العميل.
شكوى
يتابعها مركز الاتصال حتى تُحل أو يُشرح سبب تعذر حلها.
التواصل
إن لم يرد العميل ينتظر الطلب يوماً ويُحاوَل مجدداً، حتى ثلاث مرات.
حُلّت
تُغلق مع تعليق.
لم تُحل
تُغلق مع تسجيل السبب. وفي إعادة التصميم، هنا أُضيف التصعيد.
أُغلقت
تنتهي الرحلة وتذهب النتيجة إلى مستودع البيانات.
هذه المسارات تصميمي كما رسمته، بعد تصحيح أخطاء الرسم: تسمية منسوخة بين قسمين، وحلقة بلا مخرج. ولم يُضف إليها شيء.
مراجعة صريحة
إحدى عشرة ثغرة في تصميمي الأول.
قوة التصميم بقوة أضعف قاعدة فيه. وحين قرأت مخططاتي من جديد، وجدت هذه الثغرات التي كان أي نظام CRM سيصطدم بها في شهره الأول.
١. لا نموذج بيانات. قوائم وحالات، دون سجل واحد للعميل، فالشخص الواحد قد يصبح ثلاثة عملاء.
٢. التوزيع بالكلمات المفتاحية. السكربت يشترط وجود كل الكلمات معاً، ويمسح ملفه مع كل عميل جديد، ويقرأ قاموسه بالعكس، ويُسقط أي عميل لا تطابقه قواعده.
٣. تصنيف يناقض نفسه. نسب احتمال شراء بينها فجوات (٧١ إلى ٧٩٪ لا تنتمي لأي فئة)، وفترات زمنية متداخلة.
٤. لا قاعدة للإسناد. «التوزيع على الفريق» لم يحدد كيف.
٥. لا مهل. لا هدف لأول مكالمة، وثلاث محاولات دون تحديد المسافة بينها.
٦. الشكوى قد تُغلق دون حل. لا تصعيد، ولا تأكيد من العميل.
٧. أخطاء رسم. حلقة بلا مخرج، وتسمية دور منسوخة من قسم آخر، وتذكير يُرسل قبل تأكيد الحجز.
٨. أقسام مفتوحة. التسويق والأساطيل بقيا «قيد الإجراء»، وأسئلة القطع والخدمات بلا مسار.
٩. نظام الوكالة كان صندوقاً. لا تكامل: لم يُحدد ما الذي ينتقل بينه وبين CRM، ولا من يملك ماذا.
١٠. لا موافقات. تذكيرات وإعادة تواصل بالرسائل وواتساب، دون تسجيل موافقة العميل أو إتاحة إلغاء الاشتراك.
١١. فوائد بلا قياس. وُعد بخفض تكلفة الإعلان وزيادة المبيعات، دون خط أساس ولا مؤشر يثبتها.
الطبقة ٣ · إعادة التصميم
عميل واحد، وطلبات كثيرة، وكل تواصل مسجّل.
تبدأ إعادة التصميم من أضعف نقطة في التصميم الأول: البيانات. العميل سجل واحد، مفتاحه رقم جوال واحد بصيغة موحدة. وكل طلب هو عميل محتمل مرتبط بذلك العميل، وكل مكالمة أو زيارة نشاط مسجّل، والموافقات محفوظة في سجل العميل لكل قناة.
العميل
يُعرَّف برقم جوال سعودي واحد بصيغة موحدة، فالشخص نفسه القادم من ثلاث قنوات يبقى عميلاً واحداً.
الموافقة
التسويق بالرسائل وبواتساب له موافقة مستقلة لكل منهما مع تاريخها، وإلغاء الاشتراك يتقدم عليهما.
الطلب
طلب واحد: قسمه، ونوعه من النموذج، ومصدره وUTM، وحالته، وتصنيفه، ومالكه، وعدد المحاولات، ومتى رُدّ عليه أول مرة.
الموظف
ينتمي إلى قسم وفرع، ويملك العملاء المسندين إليه.
النشاط
كل مكالمة ورسالة وزيارة مع نتيجتها. وهذا ما يجعل الأداء قابلاً للقياس.
الحجز
تجربة قيادة أو زيارة صيانة، في موعد يُغلق في كل مكان حين يُحجز.
الشكوى
لها مالك، وعمر، وخطوة تصعيد، ولا تُغلق نفسها.
نظام الوكالة
يملك البيع والفاتورة وزيارة الصيانة، ويبلغ CRM بكل منها كحدث.
// One customer, many leads, and every touch recorded as an activity, so
// response times and performance are measured, not guessed.
export type Department = 'telesales' | 'testdrive' | 'aftersales' | 'callcenter' | 'fleet' | 'marketing';
export type Source = 'website' | 'social' | 'sms' | 'email' | 'outdoor' | 'event' | 'callcenter';
export interface Customer {
id: string;
phone: string; // +9665XXXXXXXX: the one key that detects duplicates
name: string;
consent: { marketingSms: boolean; marketingWhatsApp: boolean; at: string };
optedOutAt?: string;
}
export interface Lead {
id: string;
customerId: string;
department: Department;
inquiryType: string; // chosen in the form, never guessed when present
source: Source;
utm?: { campaign: string; medium: string; content?: string };
status: 'new' | 'assigned' | 'contacting' | 'qualified' | 'won' | 'lost' | 'nurture';
temperature?: 'hot' | 'warm' | 'cold' | 'lost';
ownerId?: string;
attempts: number;
lostReason?: string;
createdAt: string;
firstContactAt?: string; // speed to lead is measured from createdAt to here
}
export interface Activity {
leadId: string;
agentId: string;
kind: 'call' | 'whatsapp' | 'sms' | 'visit' | 'test_drive' | 'note';
outcome: 'answered' | 'no_answer' | 'interested' | 'not_interested' | 'booked';
at: string;
}
// The same person arrives as 0551234567, 966551234567, +966 55 123 4567 or
// 551234567. One normalised number means one customer record, not four.
export function normalizeSaudiMobile(raw: string): string | null {
const d = raw.replace(/\D/g, '');
const local = d.startsWith('966') ? d.slice(3) : d.startsWith('0') ? d.slice(1) : d;
return /^5\d{8}$/.test(local) ? `+966${local}` : null;
}
export async function findOrCreateCustomer(db: Db, input: { phone: string; name: string }) {
const phone = normalizeSaudiMobile(input.phone);
if (!phone) throw new Error('invalid_mobile');
const existing = await db.customers.findByPhone(phone);
if (existing) return { customer: existing, isNew: false };
const customer = await db.customers.insert({ phone, name: input.name.trim() });
return { customer, isNew: true };
}
الطبقة ٣ · إعادة التصميم
النموذج يقرر أين يذهب العميل. والكلمات المفتاحية تقترح فقط.
سكربت التوزيع الأول كان يقرأ النص الحر ويبحث عن كلمات مفتاحية، وهو معروض أدناه كما كتبته. كان يشترط وجود كل كلمات القسم في رسالة واحدة، فالعميل الذي كتب «شراء» فقط لا يذهب لأي مكان. وكان يفتح ملفه بوضع الكتابة، فكل عميل جديد يمسح السابق. وكانت حلقته تقرأ القاموس بالعكس. أما إعادة التصميم فتسأل العميل عن طلبه في النموذج نفسه، وتُبقي الكلمات المفتاحية للنص الحر فقط، بالعربية والإنجليزية، مع شخص يراجع كل ما هو غير واضح.
اختر نوع الطلب وتابع مساره
قائمة المبيعات الهاتفية
يُسند إلى موظف المبيعات صاحب أقل عدد من العملاء المفتوحين.
أول مكالمة خلال ١٥ دقيقة في ساعات العمل، ثم حتى ثلاث محاولات في ثلاثة أيام مختلفة.
قبل: يُحوَّل بالبريد، أو يُنسخ من أداة المهام إلى جدول مشترك.
def transfer_text_based_on_keywords(input_file, keywords_to_files):
# Read the contents of the original file
with open(input_file, 'r', encoding='utf-8') as file:
text = file.read()
# Check each set of keywords and transfer the text to the corresponding file if the keywords are found
for keywords, output_file in keywords_to_files.items():
if all(keyword in text for keyword in keywords):
with open(output_file, 'w', encoding='utf-8') as file:
file.write(text)
print(f"The text has been successfully transferred to {output_file}!")
# Define the files and the corresponding keywords
keywords_to_files = {
'TeleSales.txt': ('buy', 'cost', 'value'),
'AfterSales.txt': ('part', 'maintenance', 'inspection', 'periodic'),
'Fleet.txt': ('company', 'institution'),
}
# Use the function to transfer texts
transfer_text_based_on_keywords('path/to/input_file.txt', keywords_to_files)
import json
import re
from pathlib import Path
# The form's own "inquiry type" decides. Keywords only suggest a department
# for free text, in Arabic and English, and anything unclear goes to triage.
RULES = {
"telesales": {"buy", "price", "cost", "offer", "شراء", "سعر", "عرض", "تقسيط"},
"aftersales": {"part", "parts", "maintenance", "service", "قطع", "صيانة", "فحص"},
"fleet": {"company", "fleet", "institution", "شركة", "أسطول", "مؤسسة"},
}
def suggest_department(text: str) -> str:
words = set(re.findall(r"\w+", text.lower()))
hits = {dept: len(words & keys) for dept, keys in RULES.items()}
best = max(hits, key=hits.get)
tied = list(hits.values()).count(hits[best]) > 1
return "triage" if hits[best] == 0 or tied else best
def route(lead: dict, queue_dir: Path) -> str:
dept = lead.get("inquiry_type") or suggest_department(lead.get("message", ""))
# Append, never overwrite: every lead stays in its queue until handled.
with open(queue_dir / f"{dept}.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(lead, ensure_ascii=False) + "\n")
return dept
// The form field decides where a lead goes. Free text can only suggest, and a
// lead no rule can place goes to a triage queue that a person checks, so
// nothing is dropped and nothing is overwritten.
const BY_INQUIRY: Record<string, Department> = {
quote: 'telesales',
offer: 'telesales',
test_drive: 'testdrive',
service_booking: 'aftersales',
spare_parts: 'aftersales',
fleet: 'fleet',
complaint: 'callcenter',
general: 'callcenter',
};
export function routeLead(inquiryType: string, message = ''): Department | 'triage' {
return BY_INQUIRY[inquiryType] ?? suggestFromText(message) ?? 'triage';
}
الطبقة ٣ · إعادة التصميم
قاعدة واحدة، بلا فجوات ولا تداخل.
تصميمي الأول كان فيه تصنيفان لا يتفقان: نسب احتمال شراء بينها فجوات، وفترات شراء متداخلة. لا أحد يستطيع تقدير الاحتمال في مكالمة هاتفية، لكن العميل يستطيع أن يقول متى ينوي الشراء. لذلك تُبقي إعادة التصميم المجموعات الأربع، وتعتمد ثلاثة أسئلة يستطيع الموظف طرحها فعلاً. جرّبها.
صنّف عميلاً
متى ينوي العميل الشراء؟
هل الميزانية أو التمويل مؤكد؟
هل تم الوصول إليه خلال ثلاث محاولات؟
ساخن
معاودة الاتصال خلال يومين وعرض تجربة قيادة. ويبقى العميل مع الموظف نفسه ١٥ يوماً ثم يُعاد تقييمه.
دافئ
متابعة كل شهر حتى يقترب موعد الشراء، والمساعدة في التمويل إن كانت الميزانية هي العائق.
بارد
متابعة كل ثلاثة أشهر، والتسويق يبقى على تواصل بالعروض، بموافقة العميل فقط.
مفقود
تسجيل السبب. لا مكالمات بيع بعد ذلك، ويمكن للتسويق إعادة التواصل لاحقاً بموافقة العميل فقط.
// One rule, with no gaps and no overlaps: the purchase timeframe decides the
// temperature, a confirmed budget or financing separates hot from warm, and a
// lead nobody reached after three attempts is nurtured, not thrown away.
export function classifyLead(timeframe: Timeframe, budgetConfirmed: boolean, answered: boolean): Temperature {
if (timeframe === 'no') return 'lost';
if (!answered) return 'cold';
if (timeframe === 'd30') return budgetConfirmed ? 'hot' : 'warm';
if (timeframe === 'm3') return 'warm';
return 'cold';
}
// Days until the next follow-up for each temperature.
export const FOLLOW_UP_DAYS: Record<Temperature, number | null> = {
hot: 2, // and the same agent keeps it for 15 days before it is reassessed
warm: 30,
cold: 90,
lost: null, // the reason is recorded; only marketing may re-engage, with consent
};
الطبقة ٣ · إعادة التصميم
العميل في إعادة التصميم، من النموذج إلى نظام الوكالة.
يُستقبل مع مصدره وموافقته
النموذج يسأل العميل عن طلبه، ويسجل مصدر UTM، ويطلب الموافقة على الرسائل التسويقية منفصلة عن رسائل الخدمة.
// A service message, such as a booking reminder, needs no marketing consent.
// Offers and re-engagement do, per channel, and an opt-out wins over everything.
export function canSend(c: Customer, purpose: 'service' | 'marketing', channel: 'sms' | 'whatsapp'): boolean {
if (purpose === 'service') return true;
if (c.optedOutAt) return false;
return channel === 'sms' ? c.consent.marketingSms : c.consent.marketingWhatsApp;
}
التعرّف على العميل إن كان معروفاً
رقم الجوال يُوحَّد، فيكون ٠٥٥١٢٣٤٥٦٧ و+966 55 123 4567 الشخص نفسه، وينضمان إلى السجل نفسه.
// The same person arrives as 0551234567, 966551234567, +966 55 123 4567 or
// 551234567. One normalised number means one customer record, not four.
export function normalizeSaudiMobile(raw: string): string | null {
const d = raw.replace(/\D/g, '');
const local = d.startsWith('966') ? d.slice(3) : d.startsWith('0') ? d.slice(1) : d;
return /^5\d{8}$/.test(local) ? `+966${local}` : null;
}
export async function findOrCreateCustomer(db: Db, input: { phone: string; name: string }) {
const phone = normalizeSaudiMobile(input.phone);
if (!phone) throw new Error('invalid_mobile');
const existing = await db.customers.findByPhone(phone);
if (existing) return { customer: existing, isNew: false };
const customer = await db.customers.insert({ phone, name: input.name.trim() });
return { customer, isNew: true };
}
يُوجَّه بالنموذج، ويُفرز يدوياً عند الغموض
نوع الطلب يحدد القسم. والنص الحر يقترح فقط، وكل غامض يذهب إلى شخص.
// The form field decides where a lead goes. Free text can only suggest, and a
// lead no rule can place goes to a triage queue that a person checks, so
// nothing is dropped and nothing is overwritten.
const BY_INQUIRY: Record<string, Department> = {
quote: 'telesales',
offer: 'telesales',
test_drive: 'testdrive',
service_booking: 'aftersales',
spare_parts: 'aftersales',
fleet: 'fleet',
complaint: 'callcenter',
general: 'callcenter',
};
export function routeLead(inquiryType: string, message = ''): Department | 'triage' {
return BY_INQUIRY[inquiryType] ?? suggestFromText(message) ?? 'triage';
}
يُسند إلى الموظف الأقل انشغالاً
داخل القسم والفرع الصحيحين، يستلم العميل التالي الموظف صاحب أقل عدد من العملاء المفتوحين.
// Leads go to the agent of the right department and branch who holds the
// fewest open leads, so nobody hoards leads and nobody drowns in them.
export function pickAgent(agents: Agent[], open: Map<string, number>, branch?: string): Agent | null {
const pool = agents.filter((a) => a.active && (!branch || a.branch === branch));
if (!pool.length) return null;
return pool.reduce((best, a) => ((open.get(a.id) ?? 0) < (open.get(best.id) ?? 0) ? a : best));
}
ثلاث محاولات في ثلاثة أيام، مع مهلة
لأول مكالمة مهلة، وتجاوزها ينبّه قائد الفريق. وإن لم يرد العميل فالمحاولة التالية في يوم العمل التالي، حتى ثلاث، ثم يُرعى العميل بدل أن يُعتبر مفقوداً.
// Three attempts on three different days, inside working hours, instead of
// three calls in one afternoon. The first call has a deadline, and missing it
// alerts the team leader. (The hours here are example settings.)
const WORK_START = 9;
const WORK_END = 21;
export const FIRST_CONTACT_SLA_MIN = 15;
export function nextAttempt(lead: Lead, now: Date): Date | 'nurture' {
if (lead.attempts >= 3) return 'nurture'; // the lead turns cold; marketing keeps in touch
const next = new Date(now);
if (lead.attempts > 0) next.setDate(next.getDate() + 1);
if (next.getHours() >= WORK_END) next.setDate(next.getDate() + 1);
if (next.getHours() < WORK_START || next.getHours() >= WORK_END) next.setHours(WORK_START, 0, 0, 0);
return next;
}
export function breachedFirstContact(lead: Lead, now: Date): boolean {
if (lead.firstContactAt) return false;
return now.getTime() - Date.parse(lead.createdAt) > FIRST_CONTACT_SLA_MIN * 60_000;
}
يُصنَّف بقاعدة واحدة
ساخن أو دافئ أو بارد أو مفقود، من ثلاثة أسئلة، ولكل تصنيف موعد متابعة. والمصنِّف أعلاه يشغّل هذه القاعدة نفسها.
يُسلَّم لنظام الوكالة كحدث
حين يكتمل البيع، يرسل CRM حدثاً واحداً لنظام الوكالة، الذي يملك البيع والفاتورة وزيارات الصيانة من بعدها، ويبلغ عن كل منها.
// The CRM and the dealer management system (DMS) exchange events, not
// spreadsheets. The CRM owns the lead until it is won; the DMS owns the sale,
// the invoice and the service visit, and reports each one back.
export type CrmToDms = {
type: 'lead.won';
leadId: string;
customer: { phone: string; name: string };
model: string;
payment: 'cash' | 'finance';
};
export type DmsToCrm =
| { type: 'sale.delivered'; leadId: string; deliveredAt: string }
| { type: 'service.booked'; customerPhone: string; at: string; branch: string }
| { type: 'service.completed'; customerPhone: string; at: string; nextServiceDue?: string };
الشكوى تتصاعد بدل أن تُغلق
إن بقيت الشكوى دون حل بعد يومين تنتقل لمدير الفرع، ولا تُغلق إلا بحل وتأكيد من العميل، أو بسبب مسجّل.
// A complaint cannot close itself. Unsolved after two days it moves to the
// branch manager, and closing needs a resolution and the customer's
// confirmation, or a recorded reason why it could not be solved.
export function complaintNextStep(c: Complaint, now: Date): 'open' | 'escalate' | 'can_close' {
if (c.resolution && (c.customerConfirmed || c.unresolvableReason)) return 'can_close';
const ageHours = (now.getTime() - Date.parse(c.openedAt)) / 3_600_000;
return ageHours > 48 && !c.escalatedAt ? 'escalate' : 'open';
}
١ / ٨
الطبقة ٣ · إعادة التصميم
الوعود تصبح مؤشرات.
تصميمي الأول وعد بخفض تكلفة الإعلان وزيادة المبيعات. وإعادة التصميم تحدد كيف يُتحقق من كل وعد، من سجلات الأنشطة والعملاء، فتُقيَّم الحملة أو الفريق بالأرقام.
سرعة الرد: الدقائق من النموذج إلى أول مكالمة.
نسبة الوصول: نسبة العملاء الذين تم الوصول إليهم فعلاً.
من العميل إلى التجربة، ومن التجربة إلى البيع: أين يفقد المسار الناس.
التحويل حسب المصدر والحملة: من UTM في كل عميل، فتتبع الميزانية المبيعات الحقيقية.
نسبة عدم الحضور: لتجارب القيادة وزيارات الصيانة.
زمن حل الشكاوى والتصعيد: لكل فرع.
المتابعات المتأخرة لكل موظف: الطريقة العادلة لتقييم الأداء.
-- What the design is judged by: how fast a lead is answered, how many are
-- reached, and which source really sells, so the budget follows sales.
SELECT source,
COUNT(*) AS leads,
AVG(CASE WHEN first_contact_at IS NOT NULL THEN 1.0 ELSE 0 END) AS contact_rate,
AVG((julianday(first_contact_at) - julianday(created_at)) * 1440) AS minutes_to_first_contact,
AVG(CASE WHEN status = 'won' THEN 1.0 ELSE 0 END) AS conversion
FROM leads
WHERE created_at >= date('now', '-30 days')
GROUP BY source
ORDER BY conversion DESC;
بوضوح
ما صممته، وما هو إعادة تصميم.
صممت نظام CRM هذا بالكامل أثناء عملي محللاً للتسويق. وهذه الصفحة لا تدّعي أن الوكالة تشغّله.
نظامهم معروض كما رسمته وقتها، مع تعميم أسماء أدواتهم.
تصميمي معروض كما رسمته، بعد تصحيح أخطاء الرسم، دون إضافة شيء.
إعادة التصميم هي كيف أبنيه اليوم. وكودها مرجعي كُتب لهذه الصفحة ليُظهر الهندسة، أما سكربت التوزيع الأول فهو الأصلي من تصميمي.
لماذا أعرض الثغرات أيضاً
التصميم الذي يبدو مثالياً في عرض تقديمي ينكسر غالباً في أول يوم مزدحم. وأن أقرأ عملي من جديد، وأجد أين كان سيفشل، وأغلق كل ثغرة، هو الجزء الذي أثق به أكثر في عمل الأنظمة.
وإن كان فريق المبيعات أو الخدمة لديك يفقد العملاء بين الأدوات، يسعدني الحديث معك.