الـ API والتكامل
الطباعة على ترويسة المدرسة (قوالب التقارير)
ما هو القالب
القالب ترويسة، لا مستند. المدرسة تملك الإطار — شعارها، ترويستها، تذييلها، علامتها المائية، هوامشها، مقاس الورق واتجاهه — وتُرتّبه مرة واحدة في «منشئ التقارير» داخل أوكتا. الجسم يؤلّفه تطبيقك: القالب لا يحمل محتوى خاصاً به.
هذا هو التقسيم كاملاً:
| مَن يملكه | ماذا |
|---|---|
| المدرسة | الشعار، الترويسة، التذييل، العلامة المائية، الهوامش، الورق، كتلة الاعتماد |
| تطبيقك | محتوى المستند — العناوين والجداول والصفوف |
لماذا لا تطبع بنفسك
كل تطبيق يطبع شيئاً يصطدم بالجدار نفسه: تشكيل العربية، الاتجاه من اليمين، خطٌّ يرسمها، هوامش الصفحة، وترويسةٌ تعرف المدرسة أنها ترويستها. ومحلولاً في كل تطبيق على حدة يُحَل في كل تطبيق بشكل مختلف، فتنتهي المدرسة وفي يدها ستة مستندات بستة تخطيطات — ولا واحد منها ترويستها، والشيء الوحيد الذي أرادته، أن هذه المستندات من مدرستها، هو الشيء الذي لا يحمله أيٌّ منها.
والمحرّك يعرف ذلك كلّه أصلاً، لأن المدرسة أعدّته مرة واحدة.
متى لا يناسبك هذا: إن كان مستندك تصميماً خاصاً بك لا علاقة له بهوية المدرسة (فاتورة تحمل هويّتك أنت مثلاً)، اطبعه بنفسك. القوالب لمستندات تخرج باسم المدرسة.
النطاق المطلوب
reports.builder.read — وهو نفسه نطاق كتالوج التقارير. المدرسة تُسأل سؤالاً
واحداً: هل يقرأ هذا التطبيق سطح التقارير؟ والطباعة على ترويسة قراءة لها.
وهذا كل ما يحتاجه أغلب التطبيقات. بقيّة هذا القسم عن الطباعة على ما
رتّبته المدرسة أصلاً، ولا تحتاج معها write إطلاقاً.
والتأليف نطاق ثانٍ منفصل: reports.builder.write يسمح لتطبيقك أن
يقترح قالباً من عنده حين لا يجد في المدرسة ما يناسب مستنده — ولا يسمح له
بأن يطبع عليه: المقترَح يصل مسودةً حتى تعتمده المدرسة. تفصيله في
أن يقترح تطبيقك ترويسة.
الخطوة ١ — اسرد القوالب
GET /api/apps/reports/templates
{
"data": [
{
"id": "9wXqB4",
"name_ar": "خطاب صادر",
"name_en": "Outgoing letter",
"type": "general",
"orientation": "portrait",
"paper_size": "a4",
"header_enabled": true,
"footer_enabled": true,
"variables": [
{
"key": "letter_no",
"label_ar": "رقم الخطاب",
"label_en": "Letter number",
"type": "text",
"required": true,
"default": ""
}
]
}
]
}
id نصٌّ مبهم — استعمله كما وصلك ولا تبنِه بنفسك ولا تشتقّه من شيء.
وهو ليس ULID بخلاف بقية معرّفات هذا السطح.
اقرأ variables — هذه ليست حقلاً تتخطّاه. هي الحقول التي يطلبها هذا
القالب بعينه عند الطباعة: ترويسة تقول «رقم الخطاب: {{letter_no}}» ستطبع
فراغاً مكانه إن لم تُرسل قيمة، ولا شيء في المستند سيقول لك لماذا. وrequired
إشارتك إلى أن تسأل إنساناً بدل أن تخمّن.
تُسرد القوالب المعتمدة فقط. مدرسة في منتصف إعادة تصميم ترويستها لديها مسودة يجب ألّا تصل وليّ أمر، وتطبيقك لا يستطيع أن يميّز تصميماً منتهياً من آخر تحت التعديل بالنظر إليه — فالمنصّة لا تعرضه عليك أصلاً.
الخطوة ٢ — اطبع
POST /api/apps/reports/templates/{id}/render
{
"blocks": [
{ "type": "heading", "text": "كشف الدرجات", "level": "h1" },
{ "type": "info-row", "label": "الطالب", "value": "أحمد صالح" },
{ "type": "table",
"headers": ["المادة", "الدرجة"],
"rows": [["الرياضيات", "95"], ["العلوم", "88"]] }
],
"variables": { "letter_no": "1447-42" }
}
الجواب هو ملف PDF نفسه (Content-Type: application/pdf) لا رابطاً إليه.
الملف المخزَّن يحتاج عمراً وسياسة تنظيف وقاعدة وصول، لمستندٍ قد يحمل درجات
طفل باسمه — وتسليم البايتات يترك الثلاثة معك.
curl -X POST \
-H "Authorization: Bearer $INSTALLATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"blocks":[{"type":"heading","text":"كشف الدرجات","level":"h1"}]}' \
--output report.pdf \
https://getokta.io/api/apps/reports/templates/9wXqB4/render
الكتل المتاحة
| النوع | الحقول | ملاحظة |
|---|---|---|
heading |
text (أو html)، level: h1|h2|h3، align |
|
text |
html، align |
HTML بسيط: <p>, <strong>, <br> |
info-row |
label، value |
سطر «تسمية: قيمة» بخطّ سفلي خفيف |
table |
headers، rows، striped |
rows مصفوفة مصفوفات |
divider |
color، thickness |
|
spacer |
height (نقاط) |
|
image |
path، align، width |
path على قرص أوكتا العام — نادراً ما يملكه تطبيق خارجي |
النوع المجهول يُرفَض ولا يُتخطّى. كتلةٌ ساقطة بصمت تُخرج مستنداً ناقصاً
قسماً كاملاً لا يقول عن نفسه شيئاً، وتقريرٌ ينقصه صفٌّ أسوأ من تقرير فشلت
طباعته. الجواب 422 ونصّه يسمّي الكتلة ونوعها:
{ "error": "Block 2 has an unknown type «chart». Known types: heading, text, table, info-row, divider, spacer, image." }
المتغيّرات
القالب قد يُعلن حقولاً خاصة به، وتملؤها أنت عند الطباعة بمفاتيحها:
"variables": { "letter_no": "1447-42", "recipient": "إدارة التعليم" }
قواعد ثلاث تستحق أن تُعرف مسبقاً:
- المتغيّر غير المُعلَن يُسقَط ولا يُرفَض الطلب. بخلاف نوع الكتلة المجهول: الكتلة الساقطة تُنقص المستند قسماً، والمفتاح الزائد لا يُنقص الورقة شيئاً — فتستطيع إرسال حمولة واحدة على عدّة قوالب.
- لا يمكنك الكتابة فوق متغيّرات المنصّة (
{{tenant.name}}،{{date.today}}،{{page.number}}…). لو أمكن، لطبع تطبيقٌ اسم مدرسة أخرى على ورق هذه المدرسة. - ما لم تُرسله يعود إلى القيمة الافتراضية التي ضبطتها المدرسة، لا إلى فراغ — إن كانت لها قيمة افتراضية.
الاعتماد
للقالب دورة حياة عند المدرسة: مسودة → بانتظار الاعتماد → معتمد. ولا
يُطبع إلا المعتمد، ويُعاد الفحص عند كل طباعة لا عند السرد فقط: المُعرّف
يعيش في مخزن تطبيقك بعد أن تكون المدرسة قد أعادت القالب للتصميم.
فإذا خزّنت مُعرّف قالب، تعامل مع 422 عند الطباعة كحالة طبيعية لا
كعطل: اسرد القوالب من جديد ودع المستخدم يختار.
من تطبيق مدمج (Embedded)
التطبيق المدمج ينادي الخدمات مباشرةً في نفس العملية — بلا HTTP وبلا توكن:
use App\Services\PartnerApi\Reports\Templates\ListReportTemplates;
use App\Services\PartnerApi\Reports\Templates\RenderReportTemplate;
$templates = app(ListReportTemplates::class)();
$pdf = app(RenderReportTemplate::class)(
$templates[0]->id,
[
['type' => 'heading', 'text' => 'كشف الدرجات', 'level' => 'h1'],
['type' => 'info-row', 'label' => 'الطالب', 'value' => 'أحمد صالح'],
],
['letter_no' => '1447-42'],
);
// ['filename' => '...', 'mime' => 'application/pdf', 'bytes' => '...']
return response($pdf['bytes'], 200, [
'Content-Type' => $pdf['mime'],
'Content-Disposition' => 'attachment; filename="'.$pdf['filename'].'"',
]);
النطاق نفسه مطلوب، ويُفحص من سياق التثبيت النشط.
أن يقترح تطبيقك ترويسة
كل ما سبق يفترض أن المدرسة قد رتّبت ترويسةً تناسب مستندك. وكثيراً لا تكون قد فعلت: تطبيق درجات يحتاج كشفاً فيه الفصل الدراسي في الترويسة وتوقيعا معلّم المادة ومدير المدرسة أسفله، والمدرسة لا تعرف أن هذا ما يحتاجه — حتى تراه.
فالتطبيق يستطيع أن يقترح القالب الذي يعرف شكله، وتبقى المدرسة هي التي تقرّر.
النطاق: reports.builder.write — منفصل عن read عمداً، فتُسأل المدرسة
سؤالاً ثانياً عند التثبيت بدل أن يكتسب كلُّ تطبيق يقرأ التقارير القدرةَ على
وضع مسوّدات أمامها.
ما لا يشتريه هذا النطاق
هذه الأربع هي العقد كلّه، ولا يُنقض واحدٌ منها بأي حال:
- المقترَح يصل مسودةً ولا يُطبع. حتى تعتمده المدرسة. القالب الذي
ألّفتَه للتوّ لا يظهر في
GET /reports/templatesويُجيبPOST .../renderعليه بـ422. وهذا ليس عطلاً تلتفّ حوله: كل ما يُطبع على ترويسة يخرج باسم المدرسة إلى وليّ أمر أو وزارة، وتطبيقٌ يؤلّف ويطبع في نفس اللحظة تطبيقٌ يُصدر مستندات باسم مدرسةٍ لم يرها فيها أحد. - لا تمسّ إلا ما ألّفتَه أنت. ترويسة المدرسة التي رتّبتها بيدها، وقالبٌ
ألّفه تطبيق آخر على المدرسة نفسها — كلاهما
422عند التعديل أو الحذف. (عزل الجهات لا يقول شيئاً هنا: التطبيقان مثبَّتان على المدرسة نفسها بشرعية.) - التعديل يُسقط الاعتماد. قالبٌ اعتمدته المدرسة ثم عدّلتَه يعود مسودة ويتوقّف عن الطباعة حتى يُعتمد ثانيةً. هذا ثمنٌ حقيقي وهو المقصود: اعتمادٌ يبقى بعد التعديل يعني أن تطبيقاً يُمرّر قالباً بريئاً للاعتماد ثم يكتب فيه ما شاء.
- المعتمد لم يعد لك لتحذفه. الاعتماد هو أخذ المدرسة التصميمَ لنفسها —
وقد يكون على أوراق فصلٍ كامل. الحذف من هنا يُجيب
422، والمدرسة تحذفه من منشئ التقارير حيث تُرى العاقبة أمام من يقرّرها.
إنشاء قالب
POST /api/apps/reports/templates
{
"name_ar": "كشف درجات",
"name_en": "Marks sheet",
"orientation": "portrait",
"paper_size": "a4",
"header_center_content": "كشف درجات — الفصل {{term}}",
"footer_center_content": "{{page.number}} / {{page.total}}",
"signatures_enabled": true,
"signatures_columns": 2,
"signatures": [
{ "title_ar": "معلم المادة", "title_en": "Subject teacher" },
{ "title_ar": "مدير المدرسة", "title_en": "Principal" }
],
"variables": [
{ "key": "term", "label_ar": "الفصل الدراسي", "type": "text", "required": true }
]
}
الجواب 201 وجسمه نفس شكل القالب في السرد — بمعرّفه المبهم وبـ
variables كما سُجِّلت، فترى فوراً ما ستُطالَب بإرساله عند الطباعة.
الشعار واسم الجهة يُورَثان ولا تُرسلهما. المنصّة تنسخهما من ترويسةٍ اعتمدتها المدرسة سابقاً — لأن قالباً بترويسة فارغة أول ما تراه المدرسة يبدو تطبيقاً معطوباً لا مسودةً تنتظر شعارها. وتطبيقك لا يملك تعيينهما: ملفٌّ يصل من خارج المدرسة ليكون شعارها ليس ما يقرّره تطبيق.
ما يُقبل من حقول
قائمة سماح: كل ما ليس فيها غير قابل للكتابة من هنا.
| المجموعة | الحقول |
|---|---|
| الهوية | name_ar (مطلوب)، name_en، description، type (administrative|general) |
| الورق | orientation (portrait|landscape)، paper_size (a4|a3|letter) |
| الترويسة والتذييل | header_enabled، footer_enabled، وheader_*_content / footer_*_content للخانات right/center/left |
| العلامة المائية | watermark_text، watermark_opacity (0–100) |
| الهوامش | margin_top_mm، margin_right_mm، margin_bottom_mm، margin_left_mm (0–40) |
| كتلة الاعتماد | signatures_enabled، signatures_columns (1–4)، signatures_title، وsignatures[]: title_ar (مطلوب)، title_en، name، show_line |
| المتغيّرات | variables[]: key (مطلوب)، label_ar (مطلوب)، label_en، type (text|date|number|currency)، required، default_value |
وخارجها عمداً — والسبب يستحق أن يُقال:
header_logoوbackground_image— علامة المدرسة على ورقها. راجع أعلاه.- حالة الاعتماد ورفيقاتها — لو كانت قابلة للكتابة لاعتمد التطبيق مسوّدته بنفسه، وصار كل ما في هذا القسم زينة.
محتوى الخانات نصّ يقبل المتغيّرات: متغيّرات المنصّة ({{tenant.name}}،
{{date.today}}، {{page.number}} …) ومتغيّرات القالب التي أعلنتَها أنت.
ومفتاحٌ يظلّل متغيّراً جاهزاً يُسقَط بصمت — tenant.name مثلاً لا يصير
حقلاً تملؤه. القاعدة ليست خاصة بالتطبيقات: هي القاعدة نفسها التي يمرّ منها
محرّر المدرسة، فلا تنحرف بين البابين.
التعديل
PATCH /api/apps/reports/templates/{id}
نفس الحقول، وكلها اختيارية. الغياب يعني «اتركه كما هو» ولا يعني «امسحه» —
فتطبيقٌ نسي مفتاحاً لا يمسح تذييل المدرسة. الاستثناء الوحيد variables و
signatures: مصفوفة فارغة صريحة تعني «احذفها كلّها»، وهي الحالة التي
قال فيها المستدعي ما يقصده بوضوح.
وتذكّر البند ٣ أعلاه: بعد التعديل يتوقّف القالب عن الطباعة حتى يُعتمد ثانية.
الحذف
DELETE /api/apps/reports/templates/{id}
لمسودةٍ لم تعتمدها المدرسة. والمعتمد 422.
حدّ العدد
٢٥ قالباً لكل تثبيت. التطبيق يقترح حفنة مستندات يعرف كيف يملؤها؛ حلقةٌ
فيها خطأ تقترح آلافاً، فتكتشفها المدرسة صفحةَ منشئ تقارير لم تعد تُقرأ. تجاوز
الحدّ 422.
ماذا يحدث عند إزالة التطبيق
لا شيء يُحذف. القوالب التي اقترحها تطبيقك تبقى للمدرسة — راجعتها واعتمدتها وطبعت عليها، وهي مستنداتها الآن أيّاً كان من صاغها. تفقد نسبتها إليك فقط، وتصير قوالب مدرسة عادية.
فلا تعتمد على أن إعادة التثبيت تُعيد إليك ملكيّتها: تثبيتٌ جديد لا يملك ما ألّفه تثبيتٌ سابق، وسيحتاج أن يقترح من جديد.
التأليف من تطبيق مدمج
use App\Services\PartnerApi\Reports\Templates\CreateReportTemplate;
use App\Services\PartnerApi\Reports\Templates\UpdateReportTemplate;
use App\Services\PartnerApi\Reports\Templates\DeleteReportTemplate;
$template = app(CreateReportTemplate::class)([
'name_ar' => 'كشف درجات',
'header_center_content' => 'كشف درجات — الفصل {{term}}',
'variables' => [
['key' => 'term', 'label_ar' => 'الفصل الدراسي', 'required' => true],
],
]);
// $template->id — المعرّف المبهم نفسه الذي يعيده HTTP
متى تقترح، ومتى لا
اقترح حين يكون لمستندك شكلٌ يعرفه تطبيقك ولا تعرفه المدرسة: كشف درجات، خطاب قبول، شهادة حضور. اقترحه مرة واحدة عند أول تشغيل — لا في كل مرة يفتح فيها المستخدم شاشة، وإلا امتلأت لوحة المدرسة بمسوّدات متطابقة تنتظر اعتماداً لن يأتي.
لا تقترح حين تجد في GET /reports/templates ما يناسبك: الترويسة التي
رتّبتها المدرسة بيدها أولى دائماً، والمقترَح لا يُغني عن اختيار المستخدم.
ولا تنتظر الاعتماد في حلقة. لا شيء في السطح يخبرك متى يقع، والقرار قد يستغرق أياماً؛ الصحيح أن تسرد القوالب حين يحتاجها المستخدم فعلاً، فيظهر المعتمد وحده.
قائمة تحقق قبل النشر
-
reports.builder.readمعلَن في الـ manifest مع سبب واضح. - تسرد القوالب وتدع المستخدم يختار — لا تُثبّت مُعرّفاً في الكود.
- تقرأ
variablesوتسأل عن كلrequiredقبل الطباعة. -
422عند الطباعة يُعاد به إلى السرد، ولا يُعرض كعطل في التطبيق. - لا تفترض وجود قالب: مدرسة لم تُنشئ ولا ترويسة تُرجع
data: []، وواجهتك تحتاج ما تقوله عندها.
وإن كنت تؤلّف قوالب أيضاً:
-
reports.builder.writeمعلَن ومُبرَّر — ولا تطلبه إن كنت تطبع فقط. - تقترح مرة واحدة عند أول تشغيل، لا في كل فتح شاشة.
- تسرد أولاً وتقترح فقط حين لا تجد ما يناسبك.
- واجهتك تقول للمستخدم إن المقترَح ينتظر اعتماد المدرسة — لا تعرضه جاهزاً للطباعة، فهو ليس كذلك.
- لا حلقة انتظار للاعتماد، ولا افتراض أنه سيقع.
- تعديل قالب معتمد قرارٌ تتّخذه بعلم أنه يوقف طباعته حتى يُعتمد ثانيةً.