تخطَّ إلى المحتوى

أمثلة السكربتات

سلوكيات الربط المكتوبة بسكربت، من مادة منشورة تعمل في مساجد حقيقية: «المسار التقليدي» و«موازنة تلقائية» بقوالبهما وروابطهما وسكربتاتهما كاملةً، مع حالات اختبار جاهزة.

مشرف
الأساس

حين لا تكفي الدوال الجاهزة#

دوال النظام الخمس تنقل موضعاً أو تضبط حدّاً، ولا تعرف شرطاً ولا تتكيّف مع طالب. فإذا كانت قاعدة المسجد التثبيت أربعة أضعاف مقدار الحفظ أو حدّد مستوى الطالب من أول تسميعاته، فالجواب سكربت.

هذه الصفحة أمثلة لا مرجع لغة. اقرأ سلوكيات الربط أولاً لتفهم المصدر والهدف والحقول المراقبة والمستهدفة؛ ثم عُد إلى هنا لترى القواعد مكتوبةً بالفعل.

قالب البرنامج المنشور

نسخة مجمّدة من برنامج كامل تنشرها المنصة أو مسجد آخر: قوالبه ومناهجه وسلوكياته معاً.

أسرع طريق إلى إعدادٍ صحيح: تثبّت القالب فيصلك السكربت مربوطاً مضبوطاً بلا أن تكتب سطراً.

السلوك السكربتي

دالة واحدة اسمها onPlanChanged تستقبل ما تغيّر وتعيد { updates }.

كل السكربتات التالية على هذا الشكل، فمتى قرأت واحداً قرأتها كلها.

حقول الإعداد

مصفوفة configFields يعلنها السكربت، فتظهر للمشرف حقولاً عربية في صف الربط.

بها يعدّل المشرف قاعدةً بلا كود: يكتب ثلاثة بدل أربعة في عدد الأيام فيتغيّر سلوك عشرات الخطط.

حالات الاختبار

مصفوفة testCases تصف خططاً وهمية وما يُتوقَّع أن يكتبه السكربت عليها.

هي الفرق بين تجربةٍ على الورق وتجربةٍ على أربعين طالباً.

القرار

أي مثال يناسب مسجدك#

الأمثلة التالية ليست تمارين: اثنان منها قالبان منشوران مثبَّتان في مساجد، والثالث أدوات صغيرة تُركَّب على ما عندك.

من أين تبدأ؟

موازنة تلقائية
متقدّم

اختره حينلا تعرف مستوى الطلاب، وتريد أن يستنتجه النظام من أول تسميعاتهم ثم يوازن المسارات الثلاثة بلا تدخّل.

إنشاء برنامجك في دقيقتين: تبدأ الخطط الثلاث حرّة، ثم يحوّلها السكربت إلى ثابتة بمقدارٍ مقيس من أداء الطالب نفسه.

يحتاج تسميعات أولى نظيفة؛ وقراءات متذبذبة تؤخّر التسكين. ولا يظهر للمشرف قرارٌ يراجعه قبل وقوعه.

سكربتات مساعدة
الافتراضي

اختره حينبنيتك قائمة وتريد إضافة قاعدة واحدة: تحديد مقدار من متوسط التسميعات، أو قلب الاتجاه بالتقييم.

قصيرة يسهل فهمها وتعديلها، وتُركَّب على قالبٍ موجود دون إعادة بناء البرنامج.

تفعل شيئاً واحداً؛ ودمج عدّة قواعد يحتاج انتباهاً لترتيب الروابط.

قالب منشور

المثال الأول: «المسار التقليدي»#

قالب برنامج موثّق مثبَّت ثلاثاً وتسعين مرة، وصفه: نظام متكامل يدمج بين الحفظ، التثبيت، والمراجعة مع ميزة الربط التلقائي. وقاعدته الأساسية معلنة في وصفه: التثبيت يتبع مسار الحفظ ولا يتجاوزه أبداً.

المشكلة التي يحلّها

في الحلقة التقليدية يسمّع الطالب ثلاثة أشياء كل يوم: جديدَه، وما قرب منه، وما بعد. وموضع التثبيت ليس اختياراً مستقلاً، بل مسافةٌ خلف الحفظ تتحرّك معه. فإن ضبطه المشرف يدوياً، لزمه أن يعيد ضبطه لكل طالب بعد كل جلسة — وهذا ما يجعل الحلقة تتوقّف عن استعمال النظام بعد أسبوعين.

القاعدة بالعربية

الجدول التوضيحي المنشور مع القالب
الحالةمقدار الحفظ اليوميموضع الحفظالتأخيرموضع بدء التثبيت
المثال الأولوجه واحدالوجه ١٠٤ أوجهالوجه ٦
المثال الثانيوجهانالوجه ٢٠٨ أوجهالوجه ١٢
بعد التسميعوجهانالوجه ٢٢٨ أوجهالوجه ١٤

قوالب الخطط الثلاثة

ما في القالب المنشور
القالبالنمطالمقدارإعدادات القفزوحدة النقاط
حفظحصة ثابتة صعوداً من الناسسطر واحد — قيمةٌ يُفترض أن يتجاوزها المنهجبناءً على آخر تقدمذهب
تثبيتحصة ثابتة صعوداً٦٠ سطراً — يعيد السكربت حسابهاقفز مقدار محدد بخمسة عشر سطراًفضة
مراجعةحصة ثابتة صعوداً٣٠ سطراًبناءً على آخر تقدمبلا وحدة

كيف رُبطت السلوكيات

// قالب «تثبيت» — يراقب قالب «حفظ» (الفهرس 0)
{
  "watchedTemplateIndex": 0,
  "behaviorIndex": 0,
  "enabled": true
}

// قالب «مراجعة» — يراقب قالب «حفظ» بدالة نظام جاهزة
{
  "watchedTemplateIndex": 0,
  "platformBehaviorId": "ربط الحد الأعلى",
  "enabled": true
}
روابط القوالب كما هي في القالب المنشور — الفهرس 0 هو قالب «حفظ»

قالب حفظ بلا روابط: هو المصدر لا الهدف. وقالب تثبيت يربط سكربت تثبيت بالحفظ. وقالب مراجعة يستعمل دالة نظام جاهزة اسمها المنشور ربط الحد الأعلى، ووصفها: يقوم هذا السلوك بمراقبة تقدم أو الموضع الحالي للخطة المراقبة ويقوم بتحديث الحد الأعلى أو الحد الأدنى للخطة المستهدفة — أي أنّ المراجعة لا تحتاج سكربتاً أصلاً.

السلوك: الحقول والإعداد

الحقول المراقبة
progress وamount وdirection — أي موضع الحفظ ومقداره واتجاهه.
الحقول المستهدفة
progress وamount وdirection وstartSurah وstartAya وحدّا التنقل navigationSettings.upperBound وnavigationSettings.lowerBound وmode.
حقل الإعداد الوحيد
عدد الأيام (MULTIPLIER)، رقم، افتراضه أربعة وأدناه واحد. وهو ما يضبطه المشرف في صف الربط أو في إعدادات الربط داخل المنهج.

السكربت كاملاً

/**
 * link-behavior-config
 * watched: progress, amount, direction
 * target: progress, amount, direction, startSurah, startAya, navigationSettings.upperBound, navigationSettings.lowerBound, mode
 */
function onPlanChanged(input) {
    if (input.updatedPlan.type !== "quran" || input.targetPlan.type !== "quran") {
        return { updates: {} };
    }

    var updates = {};
    var dir = Number(input.updatedPlan.direction) || 1;

    // 1. مزامنة الاتجاه: التأكد من مطابقة اتجاه الهدف لمصدر التغيير
    if (input.targetPlan.direction !== dir) {
        updates.direction = dir;
    }

    var fullMushafNav = buildNavSettings(null);

    // 2. جلب قيمة التكوين الخاصة بـ "عدد الأيام" من الإعدادات
    var MULTIPLIER = (typeof config !== "undefined" && config && config.MULTIPLIER !== undefined)
                     ? Number(config.MULTIPLIER)
                     : 4;

    var hifzAmount = Number(input.updatedPlan.amount) || 15;

    // 3. مزامنة المقدار: مقدار الهدف = عدد الأيام × مقدار المصدر
    var targetAmount = MULTIPLIER * hifzAmount;
    if (!Number.isFinite(targetAmount) || targetAmount < 1) {
        return { updates: {}, errors: ["مقدار التثبيت غير صالح"] };
    }
    if (Number(input.targetPlan.amount) !== targetAmount) {
        updates.amount = targetAmount;
    }

    var newHifzPos = {
        sura: input.updatedPlan.progress.currentSurah,
        aya: input.updatedPlan.progress.currentAya
    };

    var currentTathbeetPos = {
        sura: input.targetPlan.progress.currentSurah,
        aya: input.targetPlan.progress.currentAya
    };

    var finalTathbeetPos = currentTathbeetPos;
    var forceReposition = false;

    var prevVerseObj = mushaf.previousVerse(newHifzPos, dir, fullMushafNav);
    var endBoundPos = prevVerseObj ? { sura: prevVerseObj.sura, aya: prevVerseObj.number } : newHifzPos;

    var minAllowedDist = MULTIPLIER * hifzAmount;
    var maxAllowedDist = (MULTIPLIER + 1) * hifzAmount;

    var isOut = mushaf.isOutOfBounds(currentTathbeetPos, dir, {
        upperBound: dir === 1 ? fullMushafNav.upperBound : endBoundPos,
        lowerBound: dir === 1 ? endBoundPos : fullMushafNav.lowerBound
    });

    if (isOut) {
        forceReposition = true;
    } else {
        var currentDist = mushaf.calculateLines(currentTathbeetPos, newHifzPos, dir, fullMushafNav);
        if (currentDist < minAllowedDist || currentDist > maxAllowedDist) {
            forceReposition = true;
        }
    }

    if (forceReposition) {
        var targetRes = mushaf.reverseNavigate({
            from: newHifzPos,
            lines: minAllowedDist,
            direction: dir,
            settings: fullMushafNav
        });
        if (!targetRes || !targetRes.verse) {
            return { updates: {}, errors: ["تعذر حساب موضع التثبيت"] };
        }

        finalTathbeetPos = {
            sura: targetRes.verse.sura,
            aya: targetRes.verse.number
        };

        updates.progress = {
            currentSurah: finalTathbeetPos.sura,
            currentAya: finalTathbeetPos.aya
        };
    }

    var isTargetFirst = finalTathbeetPos.sura < endBoundPos.sura ||
                       (finalTathbeetPos.sura === endBoundPos.sura && finalTathbeetPos.aya <= endBoundPos.aya);

    var newUpper = isTargetFirst ? { sura: finalTathbeetPos.sura, aya: finalTathbeetPos.aya } : { sura: endBoundPos.sura, aya: endBoundPos.aya };
    var newLower = isTargetFirst ? { sura: endBoundPos.sura, aya: endBoundPos.aya } : { sura: finalTathbeetPos.sura, aya: finalTathbeetPos.aya };

    var oldUpper = input.targetPlan.navigationSettings && input.targetPlan.navigationSettings.upperBound;
    var oldLower = input.targetPlan.navigationSettings && input.targetPlan.navigationSettings.lowerBound;

    if (!oldUpper || oldUpper.sura !== newUpper.sura || oldUpper.aya !== newUpper.aya) {
        updates["navigationSettings.upperBound"] = newUpper;
    }
    if (!oldLower || oldLower.sura !== newLower.sura || oldLower.aya !== newLower.aya) {
        updates["navigationSettings.lowerBound"] = newLower;
    }

    var keys = getDirectionalBoundKeys(dir);
    var startBound = keys.startBound === "upperBound" ? newUpper : newLower;
    if (Number(input.targetPlan.startSurah) !== startBound.sura) {
        updates.startSurah = startBound.sura;
    }
    if (Number(input.targetPlan.startAya) !== startBound.aya) {
        updates.startAya = startBound.aya;
    }

    // تحويل حرّة/ديناميكية → ثابتة؛ لا يُستدعى إن كانت ثابتة أصلاً
    var modeTrans = undefined;
    if (input.targetPlan.mode !== "static") {
        var r = PlanConverter.toStatic(input.targetPlan, {
            amount: targetAmount,
            reason: "sync:tathbeet-made-static",
            progress: {
                currentSurah: finalTathbeetPos.sura,
                currentAya: finalTathbeetPos.aya
            },
            startSurah: startBound.sura,
            startAya: startBound.aya,
            direction: dir,
            navigationSettings: { upperBound: newUpper, lowerBound: newLower }
        });
        if (!r.ok) {
            return { updates: {}, errors: r.errors };
        }
        modeTrans = r.modeTransition;
    }

    if (Object.keys(updates).length === 0 && !modeTrans) {
        return { updates: {} };
    }

    var output = { updates: updates };
    if (modeTrans) {
        output.modeTransition = modeTrans;
    }
    return output;
}
سكربت «تثبيت» المنشور على مستوى المنصة، بتعليقاته العربية

اقرأه في خمس خطوات: يزامن الاتجاه، ثم يحسب مقدار التثبيت بضرب عدد الأيام في مقدار الحفظ، ثم يقيس المسافة الحالية بين التثبيت والحفظ فإن خرجت عن المدى المسموح أعاد وضعه بالرجوع من موضع الحفظ، ثم يضبط حدّي التنقل وموضع البداية، ثم يحوّل الخطة إلى ثابتة إن لم تكن كذلك.

حالة اختبار للبداية

مخرجات هذا السكربت تعتمد على حساب المصحف، فابدأ بالحالة التي لا تحتاج حساباً — خطة ليست قرآنية يجب ألا يكتب فيها شيئاً — ثم أضف حالةً حقيقية بعد أن تقرأ ما كتبه فعلاً في نافذة الاختبار.

متى تختاره

  • حين يكون عندك ثلاثة مسارات معتادة، ويحدّد المشرف السرعة بنفسه لكل مستوى.
  • حين تريد نتيجةً يمكن شرحها للمعلّم في جملة: التثبيت خلف الحفظ بأربعة أيام.
  • ولا تختره إن كان مدى مراجعتك غير مشتقٍّ من الحفظ أصلاً — تلك حالة الورد الورقي، وموضعها الدعم.
«المسار التقليدي» في مكتبة قوالب البرامج
قالب منشور

المثال الثاني: «موازنة تلقائية»#

قالب موثّق مثبَّت إحدى وعشرين مرة، وصفه: برنامج يتيح لك إنشاء برنامجك في دقيقتين فقط؛ ليتولى تلقائياً معادلة مسارات الحفظ والتثبيت والمراجعة دون أي تدخل إداري، مع تحديد مستوى الطالب بدقة عالية.

المشكلة التي يحلّها

المسار التقليدي يفترض أنّك تعرف مقدار الطالب اليومي قبل أن يسمّع. وفي الواقع لا يُعرف مستوى الطالب الجديد إلا بعد جلسات، فيضع المشرف تقديراً ثم يعيد ضبطه لكل طالب على حدة. هذا القالب يقلب الترتيب: تبدأ الخطط الثلاث حرّة بلا مقدار، ويقيس السكربت أوّل تسميعات الطالب، فإذا استقرّت حوّل خطته إلى ثابتة بمقدارٍ مأخوذ من أدائه هو.

القاعدة بالعربية

قوالب الخطط وروابطها

الفرق الجوهري عن المسار التقليدي
القالبالنمط عند الإنشاءمن يراقب ماذاوحدة النقاط
حفظحصة حرة صعوداً من الناسيراقب نفسه بسلوك تحديد المستوى التلقائيذهب
تثبيتحصة حرةيراقب حفظ بسلوك تثبيت وعدد الأيام ثلاثةفضة
مراجعةحصة حرةرابطان: ربط الحد الأعلى على حفظ، وتحديد المستوى التلقائي على نفسهافضة
// قالب «حفظ» (الفهرس 0) — يراقب نفسه
{
  "watchedTemplateIndex": 0,
  "platformBehaviorId": "تحديد المستوى التلقائي",
  "enabled": true,
  "config": { "observationCount": 2, "tolerancePercent": 10 }
}

// قالب «تثبيت» (الفهرس 1) — يراقب «حفظ»
{
  "watchedTemplateIndex": 0,
  "platformBehaviorId": "تثبيت",
  "enabled": true,
  "config": { "MULTIPLIER": 3 }
}

// قالب «مراجعة» (الفهرس 2) — رابطان
{
  "watchedTemplateIndex": 0,
  "platformBehaviorId": "ربط الحد الأعلى",
  "enabled": true,
  "config": { "syncDirection": true }
}
{
  "watchedTemplateIndex": 2,
  "platformBehaviorId": "تحديد المستوى التلقائي",
  "enabled": true,
  "config": { "observationCount": 2, "tolerancePercent": 10 }
}
روابط القوالب الثلاثة — لاحظ الرابط الذي يشير إلى فهرس قالبه نفسه

السلوك: الحقول والإعداد

الحقول المراقبة
progress وnumberOfActivities — يكفي أن يسجَّل نشاط جديد.
الحقول المستهدفة
amount وmode، إضافةً إلى مفاتيح metadata التي يخزّن فيها القراءات المتراكمة.
عدد التلاوات للمراقبة
observationCount — افتراضه خمسة، وقالب موازنة تلقائية يخفضه إلى اثنين ليسكّن الطالب بسرعة.
نسبة التفاوت المقبولة (±%)
tolerancePercent — افتراضه عشرة. كلّما رفعته قبِلت قراءاتٍ أبعد عن الوسيط، فاستقرّ المقدار أسرع وأقلّ دقة.

السكربت كاملاً

/**
 * link-behavior-config
 * watched: progress, numberOfActivities
 * target: amount, metadata, mode
 */function onPlanChanged(input) {
  if (input.changeKind !== "activity") return { updates: {} };
  if (input.trigger.action !== "created") return { updates: {} };

  var meta = input.targetPlan.metadata || {};

  if (meta.evaluationDone) return { updates: {} };

  var updates = {};
  var modeTrans = undefined;

  // التأكد من أن الخطة حرة أثناء التقييم
  if (input.targetPlan.mode !== "free") {
     var freeResult = PlanConverter.toFree(input.targetPlan, { reason: "adaptive:evaluation-phase" });
     if (freeResult.ok) modeTrans = freeResult.modeTransition;
  }

  var amounts = meta.recitationAmounts || [];
  var currentAmount = input.trigger.activity.amount;

  if (currentAmount > 0) amounts.push(currentAmount);

  if (amounts.length < config.observationCount) {
    updates["metadata.recitationAmounts"] = amounts;
    var out = { updates: updates };
    if (modeTrans) out.modeTransition = modeTrans;
    return out;
  }

  // حساب الوسيط
  var sorted = amounts.slice().sort(function(a, b) { return a - b; });
  var mid = Math.floor(sorted.length / 2);
  var median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0;

  var tolerance = config.tolerancePercent / 100.0;
  var minAccepted = median * (1 - tolerance);
  var maxAccepted = median * (1 + tolerance);

  var validAmounts = [];
  var sum = 0;
  for (var i = 0; i < amounts.length; i++) {
    if (amounts[i] >= minAccepted && amounts[i] <= maxAccepted) {
      validAmounts.push(amounts[i]);
      sum += amounts[i];
    }
  }

  // --- شرط الاستقرار الجديد ---
  // يجب أن يكون 60% على الأقل من القراءات (3 من 5) ضمن النطاق المقبول
  var requiredStableCount = Math.ceil(config.observationCount * 0.6);
  
  if (validAmounts.length < requiredStableCount) {
      // القراءات متذبذبة! نحذف أقدم قراءة وننتظر تلاوة جديدة (Rolling Window)
      amounts.shift(); 
      updates["metadata.recitationAmounts"] = amounts;
      var pendingOut = { updates: updates };
      if (modeTrans) pendingOut.modeTransition = modeTrans;
      return pendingOut;
  }

  // القراءات مستقرة! نحسب المتوسط ونحول الخطة
  var average = sum / validAmounts.length;
  var finalAmount = Math.round(average); 

  var staticResult = PlanConverter.toStatic(input.targetPlan, {
    amount: finalAmount,
    reason: "adaptive:evaluation-finished"
  });

  if (!staticResult.ok) return { updates: {}, errors: staticResult.errors };

  updates["metadata.recitationAmounts"] = null;
  updates["metadata.evaluationDone"] = true;

  return {
    updates: updates,
    modeTransition: staticResult.modeTransition
  };
}
سكربت «تحديد المستوى التلقائي» المنشور على مستوى المنصة

حالات اختبار جاهزة

[
  {
    "name": "لا يعمل إلا مع نشاط جديد",
    "input": {
      "changeKind": "manual",
      "updatedFields": ["progress"],
      "oldPlan":     { "id": "w", "type": "quran" },
      "updatedPlan": { "id": "w", "type": "quran" },
      "targetPlan":  { "id": "t", "type": "quran", "mode": "free", "metadata": {} }
    },
    "expectedOutput": {}
  },
  {
    "name": "يخزّن أول تلاوة ولا يحوّل الخطة بعد",
    "input": {
      "changeKind": "activity",
      "trigger": { "action": "created", "activity": { "amount": 15 } },
      "updatedFields": ["progress", "numberOfActivities"],
      "oldPlan":     { "id": "w", "type": "quran" },
      "updatedPlan": { "id": "w", "type": "quran" },
      "targetPlan":  { "id": "t", "type": "quran", "mode": "free", "metadata": {} }
    },
    "expectedOutput": { "metadata.recitationAmounts": [15] }
  }
]
الصقها في «حالات الاختبار» بعد حفظ السلوك

متى تختاره

  • حين يدخل الطلاب دفعةً واحدة ولا وقت لتقدير مستوى كل واحد.
  • حين يقبل المسجد أن يقرّر النظام المقدار بدل المشرف.
  • ولا تختره إن كانت سرعات المستويات مقرّرةً في ورقٍ معتمد — عندئذٍ المناهج أصدق من القياس.
أمثلة

سكربتان قصيران يُركَّبان على ما عندك#

ليس كل سكربت خمسة آلاف حرف. هذان مثالان منشوران لا يتجاوز أحدهما صفحةً، ويصلحان قاعدةً تبني عليها سكربتك الأول.

«التقييم التكيفي لمستوى الطالب»

وصفه المنشور: يقوم السلوك بتحليل أول 4 جلسات تسميع للطالب تلقائياً، مع استبعاد النتائج الشاذة، لتحديد متوسط أداء الطالب واعتماده كمقدار يومي مقترح للخطة. يراقب totalGainedAmount وحده، ويكتب amount وحده، وحقل إعداده عدد التسميعات المطلوبة افتراضه أربعة.

/**
 * link-behavior-config
 * watched: totalGainedAmount
 * target: amount, metadata.initialRecitations, metadata.hasDeterminedLevel, metadata.lastTotalGained
 */function onPlanChanged(input) {
  if (input.updatedPlan.type !== "quran") return { updates: {} };
  
  var meta = input.targetPlan.metadata || {};
  if (meta.hasDeterminedLevel) {
    return { updates: { "metadata.lastTotalGained": input.updatedPlan.totalGainedAmount } };
  }

  var gained = input.updatedPlan.totalGainedAmount - (meta.lastTotalGained || 0);
  if (gained <= 0) return { updates: {} };

  var recitations = meta.initialRecitations || [];
  recitations.push(gained);
  
  var updates = {
    "metadata.lastTotalGained": input.updatedPlan.totalGainedAmount,
    "metadata.initialRecitations": recitations
  };

  var targetCount = (typeof config !== 'undefined' && config.targetSessions !== undefined) ? config.targetSessions : 4;

  // التحقق من التقارب فقط عند الوصول للعدد المطلوب أو أكثر
  if (recitations.length >= targetCount) {
    // نأخذ آخر 4 تسميعات للتحقق من تقاربها
    var lastFour = recitations.slice(-targetCount);
    var min = Math.min.apply(null, lastFour);
    var max = Math.max.apply(null, lastFour);
    var sum = 0;
    for (var k = 0; k < lastFour.length; k++) sum += lastFour[k];
    var avg = sum / lastFour.length;

    // شرط التقارب: الفرق بين الأكبر والأصغر لا يتجاوز 40% من المتوسط (قابل للتعديل)
    if ((max - min) <= (avg * 0.4)) {
      updates.amount = Math.round(avg);
      updates["metadata.hasDeterminedLevel"] = true;
    }
  }

  return { updates: updates };
}
سكربت «التقييم التكيفي لمستوى الطالب»

الفرق بينه وبين تحديد المستوى التلقائي: هذا يقيس الفارق بين الأكبر والأصغر ولا يحوّل نمط الخطة، وذاك يستعمل الوسيط ونافذةً متدحرجة ويحوّل الخطة إلى ثابتة. فاختر الأول إن كانت خططك ثابتة أصلاً وتريد ضبط مقدارها فحسب.

«تحديد الإتجاه»

أقصر مثال مفيد: يراقب الموضع، ويكتب direction وحده، وبلا حقول إعداد. يقرأ تقييم الجلسة، فإن كان ضعيفاً قلب المسار صعوداً نحو قصار السور، وإن كان ممتازاً قلبه نزولاً.

/**
 * link-behavior-config
 * watched: progress
 * target: direction
 */

function onPlanChanged(input) {
  // التأكد من أن التغيير ناتج عن نشاط مسجل
  if (input.changeKind !== "activity" || input.trigger.action !== "created") {
    return { updates: {} };
  }

  var act = input.trigger.activity;

  // التأكد من وجود تقييم للنشاط
  if (typeof act.rating !== "number") {
    return { updates: {} };
  }

  var currentDirection = input.targetPlan.direction;
  var newDirection = currentDirection;

  // تقييم 90 أو أقل (خصم 10 فأكثر) -> من الناس للفاتحة
  if (act.rating <= 90) {
    newDirection = -1;
  } 
  // تقييم 107 أو أعلى (زيادة 7 فأكثر) -> من الفاتحة للناس
  else if (act.rating >= 107) {
    newDirection = 1;
  }

  // تحديث الاتجاه فقط إذا كان هناك تغيير فعلي
  if (newDirection !== currentDirection) {
    return { updates: { direction: newDirection } };
  }

  return { updates: {} };
}
سكربت «تحديد الإتجاه»
[
  {
    "name": "تقييم ضعيف يقلب الاتجاه صعوداً",
    "input": {
      "changeKind": "activity",
      "trigger": { "action": "created", "activity": { "rating": 85 } },
      "updatedFields": ["progress"],
      "oldPlan":     { "id": "w", "type": "quran" },
      "updatedPlan": { "id": "w", "type": "quran" },
      "targetPlan":  { "id": "t", "type": "quran", "direction": 1 }
    },
    "expectedOutput": { "direction": -1 }
  },
  {
    "name": "تقييم متوسط لا يغيّر شيئاً",
    "input": {
      "changeKind": "activity",
      "trigger": { "action": "created", "activity": { "rating": 100 } },
      "updatedFields": ["progress"],
      "oldPlan":     { "id": "w", "type": "quran" },
      "updatedPlan": { "id": "w", "type": "quran" },
      "targetPlan":  { "id": "t", "type": "quran", "direction": 1 }
    },
    "expectedOutput": {}
  }
]
حالتا اختبار لـ«تحديد الإتجاه»
المرجع

تشريح أي سكربت: القالب الذي لا يتغيّر#

كل سكربت في هذه الصفحة — ومنها الخمسة آلاف حرف — مبنيٌّ على الهيكل نفسه. متى عرفت أجزاءه الأربعة، صرت تقرأ أي سلوك وتحكم عليه.

/**
 * link-behavior-config
 * watched: progress
 * target: progress, navigationSettings.lowerBound
 */
const configFields = [
  {
    "id": "sessionWindow",
    "name": "عدد جلسات الحفظ الأخيرة",
    "type": "number",
    "required": false,
    "default": 3,
    "validation": { "min": 1 }
  }
];

function onPlanChanged(input) {
  if (input.updatedPlan.type !== "quran") return { updates: {} };
  if (!input.updatedFields.includes("progress")) return { updates: {} };

  // أسطر التأخير = عدد الجلسات × مقدار جلسة الحفظ
  var offset = config.sessionWindow * (input.updatedPlan.amount || 15);
  return COPY_PROGRESS({ positionOffset: offset, stepBackward: true });
}
الهيكل المطلوب: تعليق الإعداد، ثم حقول الإعداد، ثم الدالة
الأجزاء الأربعة
الجزءوظيفتهقاعدة لا تُخالَف
تعليق link-behavior-configيعلن الحقول المراقبة والمستهدفة، فتؤشّرها الواجهة تلقائياً.يجب أن يذكر كل مفتاح يكتبه السكربت، وإلا أُهمل عند التشغيل.
configFieldsيعرّف حقول المشرف العربية وقيمها الافتراضية.الاسم configFields حرفياً، وما بعد المساواة JSON صارم بلا تعليقات ولا فاصلة زائدة، ولكل حقل قيمة افتراضية.
onPlanChanged(input)الدالة التي تعمل عند كل تغيّر مراقَب.تبدأ بحُرّاس: نوع الخطة، ونوع التغيير، وهل الحقل المعني ضمن ما تغيّر.
testCasesحالات تُلصق في نافذة الاختبار، ولا تُحفظ مع السلوك.حالةٌ ناجحة، وحالةٌ لا يعمل فيها السلوك، وحالةٌ حدّية — ثلاثٌ تكفي.

ماذا يصل إلى الدالة

input.changeKind
سبب التشغيل: activity لنشاط، أو manual لتعديل يدوي، أو link_propagation لانتشار من سلوك آخر، أو land عند إنشاء الخطة أو إعادة تطبيق المنهج.
input.updatedFields
ما تغيّر في المصدر. وقد تكون فارغة في حالة land — والسلوك يعمل رغم ذلك، فاحترس من حارسٍ يعتمد عليها وحدها.
input.oldPlan وinput.updatedPlan
خطة المصدر قبل التغيير وبعده.
input.targetPlan
الخطة التي ستُكتب فيها المخرجات.
input.trigger.activity
حين يكون السبب نشاطاً: amount وrequiredAmount وrating، ومع التسميع مواضع البداية والنهاية.
config
قيم حقول الإعداد بعد دمجها: الافتراضات، ثم إعداد الربط في القالب، ثم إعداد الخطة نفسها.

ماذا تعيد الدالة

  • { updates: {} } تعني: لا شيء يتغيّر. وهي الجواب الصحيح في أكثر الاستدعاءات.
  • updates تقبل المفاتيح المعلنة في target وحدها، ومنها مفاتيح metadata.* لحفظ الحالة.
  • errors مصفوفة رسائل تُسجَّل حين يتعذّر الحساب.
  • modeTransition هو الطريق الوحيد لتغيير نمط الخطة، ولا يأتي إلا من PlanConverter.
محرّر السكربت: التعليق يولّد الحقول المؤشَّرة تلقائياً
أسئلة شائعة

أسئلة شائعة

هل أحتاج أن أكتب سكربتاً لأستفيد من هذه الأمثلة؟

لا. المسار التقليدي وموازنة تلقائية قالبا برنامج منشوران: أنشئ برنامجاً من قالب فيصلك كل شيء مربوطاً. ولا تحتاج الكود إلا إن أردت تعديل القاعدة نفسها.

عدّلت رقماً في السكربت فتوقّف عن العمل — ما أوّل ما أفحصه؟

تعليق link-behavior-config في أعلى السكربت. إن كتبت مفتاحاً غير مذكور في سطر target أُهمل في صمت بلا رسالة خطأ.

كيف أغيّر مسافة التثبيت من أربعة أيام إلى ثلاثة؟

لا تمسّ السكربت. غيّر قيمة عدد الأيام في صف الربط داخل قالب تثبيت، أو في إعدادات الربط داخل المنهج إن أردت قيمةً لمستوى بعينه.

ما الفرق بين سلوك عام وسلوك مسجد؟

السلوك العام تنشره المنصة ويتاح لكل المساجد للاستعمال في روابط القوالب، وسلوك المسجد ينشئه مسؤولوه ويعدّلونه محلياً. والقوالب المنشورة تشير إلى السلوكيات العامة بمعرّفاتها، فتصلك مع القالب.

هل يمكنني نسخ سكربت من هنا ولصقه كما هو؟

نعم، السكربتات هنا كاملة كما تعمل. والشرط أن تلصقه كاملاً — فالسكربت المقتطع أخطر من الخطأ الصريح — وأن تُعلن حقوله المراقبة والمستهدفة كما في تعليقه، ثم تجرّبه في نافذة الاختبار قبل ربطه.